Showing preview only (285K chars total). Download the full file or copy to clipboard to get everything.
Repository: kylemath/EEGEdu
Branch: master
Commit: 4a2628127b74
Files: 61
Total size: 264.2 KB
Directory structure:
gitextract_dr3zp5xw/
├── .firebaserc
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows/
│ └── workflow.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── app.yaml
├── catalogue.json
├── firebase.json
├── package.json
├── public/
│ ├── index.html
│ └── manifest.json
└── src/
├── components/
│ ├── App/
│ │ ├── App.js
│ │ └── translations/
│ │ └── en.json
│ └── PageSwitcher/
│ ├── PageSwitcher.js
│ ├── components/
│ │ ├── EEGEduAlpha/
│ │ │ ├── EEGEduAlpha.js
│ │ │ ├── sketchFixation.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduAnimate/
│ │ │ ├── EEGEduAnimate.js
│ │ │ ├── sketchBands.js
│ │ │ ├── sketchCube.js
│ │ │ ├── sketchDraw.js
│ │ │ ├── sketchFlock.js
│ │ │ ├── sketchFlock3D.js
│ │ │ ├── sketchTone.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduBands/
│ │ │ ├── EEGEduBands.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduEvoked/
│ │ │ ├── EEGEduEvoked.js
│ │ │ ├── sketchEvoked.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduHeartRaw/
│ │ │ ├── EEGEduHeartRaw.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduHeartSpectra/
│ │ │ ├── EEGEduHeartSpectra.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduIntro/
│ │ │ ├── EEGEduIntro.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduPredict/
│ │ │ ├── EEGEduPredict.js
│ │ │ ├── sketchPredict.js
│ │ │ ├── sketchPredictSound.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduRaw/
│ │ │ ├── EEGEduRaw.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduSpectra/
│ │ │ ├── EEGEduSpectra.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduSpectro/
│ │ │ ├── EEGEduSpectro.js
│ │ │ ├── sketchSpectro.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduSsvep/
│ │ │ ├── EEGEduSsvep.js
│ │ │ ├── sketchFlashFast.js
│ │ │ ├── sketchFlashSlow.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── chartOptions.js
│ │ └── translations/
│ │ └── en.json
│ ├── translations/
│ │ └── en.json
│ └── utils/
│ ├── chartUtils.js
│ └── mockMuseEEG.js
└── index.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .firebaserc
================================================
{
"projects": {
"default": "eegedu"
}
}
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/workflows/workflow.yml
================================================
# .github/workflows/nodejs.yml
name: Build and Deploy to Firebase
on:
push: # Run on Push Requests
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x] # Only run the 10.x build
steps:
- name: Checkout Repo
uses: actions/checkout@master
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@master
with:
node-version: ${{ matrix.node-version }}
- name: Install Yarn
run: npm install -g yarn
- name: Install Dependencies
run: yarn install
- name: Build
run: yarn build
- name: Create artifact dir
run: |
mkdir -p artifacts
- name: Archive Produciton Artifacts
uses: actions/upload-artifact@master
with:
name: build
path: build
deploy:
name: Deploy to Firebase
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@master
- name: Download Production Artifacts
uses: actions/download-artifact@master
with:
name: build
- name: Deploy to Firebase
uses: w9jds/firebase-action@master
with:
args: deploy --only hosting
env:
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
================================================
FILE: .gitignore
================================================
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# editor
/.idea
.firebase
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at kylemath@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
================================================
FILE: CONTRIBUTING.md
================================================
🎉 Thanks for thinking about making a contribution! 🎉
The following is a set of guidelines for contributing to `EEGEdu`. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
# Issues
To start, you can find all the open issues [here](https://github.com/kylemath/EEGEdu/issues).
# Pull Requests (PR)
At any point in time you can create a pull request, so others can see your changes and give you feedback.
# Setup
## Fork the Repository
Start with a fork of the repository so you can work worry-free on your own fork.
You will need to fork once. Then, you can call `git fetch upstream` and `git pull 'branch-name'` before you do your local changes to fetch the latest remote changes make sure your changes are being made on up-to-date source code.
## Make Development Environment and Change Away
Configure your Dev Environment and make your changes on your fork.
# Contributing
## Creating a Pull Request
Once you have made your changes, you can create a pull request, so others can see your changes and give you feedback. Create all pull requests on the master branch. If your PR is a work-in-progress, add a [WIP] at the start of the PR title. Example:[WIP] Heart Rate Monitor Module.
## Wait for Reviews
After each PR others will look into your feature, discuss the changes in the PR, and then merge into the master branch when changes are ready.
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 Kyle Mathewson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# EEGEdu
<p align="center">
<a href="https://github.com/kylemath/EEGEdu/blob/master/LICENSE" target="_blank">
<img src="https://img.shields.io/badge/License-MIT-brightgreen.svg" alt="MIT license" />
</a>
<img src="https://img.shields.io/github/v/release/kylemath/EEGEdu?include_prereleases" />
<a href="https://github.com/kylemath/EEGEdu/blob/master/CONTRIBUTING.md" target="_blank">
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs welcome!" />
</a>
<img src="https://img.shields.io/github/issues-raw/kylemath/EEGEdu" alt="issues" />
<img src="https://img.shields.io/github/issues-closed-raw/kylemath/EEGEdu" />
<a href="https://neurotechx.herokuapp.com/" target="_blank">
<img src="https://neurotechx.herokuapp.com/badge.svg">
</a>
<a href="https://neurotechx.slack.com/messages/eegedu/" target="_blank">
<img src="https://img.shields.io/badge/NeurotechXChannel-%23eegedu-lightgrey" />
</a>
</p>
<p align="center">
<img src="logo.png" alt="Interactive Brain Playground Logo" width=500 />
</p>
`EEGEdu` is an Interactive Brain Playground.
`EEGEdu` is served live at [https://eegedu.com/](https://eegedu.com). This website is served from the software in this repository. So, all you need to do to try the system out is head to [EEGEdu](https://eegedu.com/).
`EEGEdu` is designed as an interactive educational website to learn/teach about working with electroencephalogram (EEG) data. It is a teaching tool that allows for students to interact with their own brain waves.
`EEGEdu` has been inspired by multiple works that came before. It is inspired by [EEG101](https://github.com/NeuroTechX/eeg-101), but `EEGEdu` is web-based. Being web-based allows students to interact with EEG brain data without having to install any software. Others have used [Neurotech EEG-notebooks in python](https://github.com/NeuroTechX/eeg-notebooks) for data collection and analysis with [muse-lsl](https://github.com/alexandrebarachant/muse-lsl). These software support the Interaxon MUSE headset but require a bluetooth low-energey (BLE) dongle to work with common operating systems (e.g. Windows or Mac OSX). These tools also required the editing `pyglatt` code to connect to Muse headsets. Thus, previous software are cumbersome and serve as a barrier to entry for many students learning about EEG. `EEGEdu` aims to provide students with an accesible introduction to working with their own brain waves.
# EEGEdu Curriculum
EEGEdu provides an step-by-step incremental tutorial for students to interact with EEG-based brain signals. So, we break down the curriculum into 10 lessons as follows:
1. [Connect + hardware, Biophysics + signal viewing](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduIntro/EEGEduIntro.js)
2. [Heart rate time domain data](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduHeartRaw/EEGEduHeartRaw.js)
3. [Heart rate frequency domain -beats per minute](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduHeartSpectra/EEGEduHeartSpectra.js)
4. [Raw Data + artifacts + blinks + record](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduRaw/EEGEduRaw.js)
5. [Frequency domain explanation + Raw spectra + record](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduSpectra/EEGEduSpectra.js)
6. [Frequency bands + record](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduBands/EEGEduBands.js)
7. [Spectrogram](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduSpectro/EEGEduSpectro.js)
8. [Neurofeedback p5js demos](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduAnimate/EEGEduAnimate.js)
9. [Eyes closed eyes open experiment](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduAlpha/EEGEduAlpha.js)
10. [SSVEP experiment](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduSsvep/EEGEduSsvep.js)
11. [BCI trainer](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduPredict/EEGEduPredict.js)
# Installation for Development
If you are interested in developing EEGEdu, here are some instructions to get the software running on your system. *Note*: Currently EEGEdu development requires a Mac OSX operating system.
To start, you will need to install [Homebrew](https://brew.sh) and [yarn](https://yarnpkg.com/lang/en/docs/install/#mac-stable). These are easy to install with the following Terminal / `bash` commands:
```sh
## Install homebrew
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
## Install yarn
# NOTE: this will also install Node.js if it is not already installed.
brew install yarn
# NOTE: Node.js must be version 10.x for Muse interaction
# Thus, if you are getting version issues, install n, with the following command:
# sudo npm install -g n
# Then, you can switch to version 10.x with the following command:
# sudo n 10.16.0
```
Then, in Terminal/`bash`, clone this Git repository and change directory into the newly cloned folder:
```sh
git clone https://github.com/kylemath/EEGEdu
cd EEGEdu
```
Then, you can install the required `yarn` packages for EEGEdu:
```sh
yarn install
```
## Local Development Environment
Then, you can run the *Local Development Environment* of EEGEdu:
```sh
yarn start dev
```
If it is working correctly, the EEGEdu application will automatically open in a browser window at http://localhost:3000.
## Local Production Environment
To start the *Local Production Environment*, you can use the following commands:
```sh
yarn cache clean
yarn run build
serve -s build
```
## Local Testing of Changes
1. Install any new packages `yarn install`
1. Start the *Local Development Environment* `yarn start dev`
1. Look for errors in terminal log
1. Open's browser to http://localhost:3000
1. Open Javascript console
1. Look for errors in console
1. Connect Mock data stream by clicking Connect button
1. Run through the `checkFunction` below with Mock data.
1. Disconnect Mock data stream
1. Turn on Interaxon Muse headband
1. Connect Muse data stream
1. Repeat the `checkFunction` below with Muse data.
```sh
# Pseudocode for checking EEGEdu functionality
checkFunction = {
view raw data
change sliders
make sure data changes and no errors
click spectra
move sliders
make sure changes
click bands
move sliders
make sure changes
}
```
## Deployment
[EEGEdu](https://eegedu.com) is running on [Firebase](https://firebase.google.com/) and deployment happens automagically using GitHub post-commit hooks, or [Actions](https://github.com/kylemath/EEGEdu/actions), as they are commonly called. You can see how the application is build and deployed by [inspecting the workflow](https://github.com/kylemath/EEGEdu/blob/master/.github/workflows/workflow.yml).
Currently this automagic deployment is not working, so we can deploy to firebase manually:
First, install the Firebase deployment tools:
```sh
sudo brew install firebase
sudo yarn global add firebase-tools
sudo yarn global add firebase
```
The first deployment requires login and initialization once:
```sh
firebase login
```
Browser opens, and login to Google account authorized for Firebase deployment:
```sh
firebase init
```
* options: Hosting Sites only
* public directory: build
* single-page app: No
* Overwrite - No
* Overwrite - No
Then, deployment to Firebase happens with the following commands:
```sh
# clean the local cache to ensure most recent version is served
yarn cache clean
# build the latest version of the site
yarn run build
# deploy the latest version to firebase
firebase deploy
```
# References and Related Tools
* [Muse 2016 EEG Headset JavaScript Library (using Web Bluetooth)](https://github.com/urish/muse-js)
* [Muse 2016 + Web Bluetooth demo app in Angular](https://github.com/NeuroJS/angular-muse)
* [Explore Muse headband data in frequency domain](https://github.com/tanvach/muse-fft)
* [Pipeable RxJS operators for working with EEG data in Node and the Browser](https://github.com/neurosity/eeg-pipes)
* [React, A JavaScript library for building user interfaces](https://reactjs.org/)
* [Simple yet flexible JavaScript charting for designers & developers](https://www.chartjs.org/docs/latest/)
* [Muse 2016 EEG Headset LSL (NodeJS)](https://github.com/urish/muse-lsl)
* [Electroencephalogram (EEG) Recording Protocol for Cognitive and Affective
Human Neuroscience Research](https://static1.squarespace.com/static/5abefa62d274cb16de90e935/t/5df7db5956ec9170a9b402e1/1576524637645/Electrode_Application_Protocol_Final_Compressed.pdf)
# Contributing
The guide for contributors can be found [here](https://github.com/kylemath/EEGEdu/blob/master/CONTRIBUTING.md). It covers everything you need to know to start contributing to EEGEdu.
# Credits
`EEGEdu` - An Interactive Electrophysiology Tutorial with the Interaxon Muse brought to you by Mathewson Sons. A [Ky](http://kylemathewson.com)[Kor](http://korymathewson.com)[Key](http://keyfer.ca) Production.
# License
[EEGEdu is licensed under The MIT License (MIT)](https://github.com/kylemath/EEGEdu/blob/master/LICENSE)
## Preview
<p align="center">
<img src="screenshot.png" alt="Project screenshot" width="720" />
</p>
================================================
FILE: app.yaml
================================================
runtime: nodejs10
env: standard
================================================
FILE: catalogue.json
================================================
{
"id": "EEGEdu",
"title": "EEGEdu",
"oneLiner": "An interactive website to learn about brain waves",
"demoUrl": "https://eegedu.com/",
"screenshot": "https://raw.githubusercontent.com/kylemath/EEGEdu/master/screenshot.png",
"kind": "page",
"categories": ["eeg", "education", "neuroscience"],
"tags": ["eeg", "education", "neuroscience"],
"status": "published"
}
================================================
FILE: firebase.json
================================================
{
"hosting": {
"headers": [
{ "source":"/service-worker.js", "headers": [{"key": "Cache-Control", "value": "no-cache"}] }
],
"public": "build",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}
================================================
FILE: package.json
================================================
{
"engines": {
"npm": "5.x",
"node": "10.x"
},
"name": "eegedu",
"version": "0.1.0",
"private": true,
"dependencies": {
"@neurosity/pipes": "^3.0.2",
"@shopify/polaris": "^4.9.0",
"chart.js": "^2.7.2",
"file-saver": "^2.0.2",
"firebase": "^7.5.0",
"firebase-tools": "^7.9.0",
"handlebars": "^4.3.0",
"ml5": "^0.4.3",
"muse-js": "^3.0.1",
"p5": "^0.10.2",
"p5.js-widget": "https://github.com/toolness/p5.js-widget.git",
"prettier": "^1.19.1",
"react": "^16.12.0",
"react-chartjs-2": "^2.7.2",
"react-dom": "^16.4.1",
"react-p5-wrapper": "^2.0.0",
"react-scripts": "^3.2.0",
"react-youtube": "^7.9.0",
"rxjs": "^6.5.3"
},
"scripts": {
"start": "react-scripts --max_old_space_size=4096 start",
"build": "react-scripts --max_old_space_size=4096 build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
================================================
FILE: public/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<title>EEGEdu</title>
<link rel="stylesheet" href="https://unpkg.com/@shopify/polaris@4.9.0/styles.min.css" />
</head
>
<body>
<div id="root"></div>
<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/7.6.1/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.6.1/firebase-performance.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.6.1/firebase-analytics.js"></script>
<script>
const firebaseConfig = {
apiKey: "AIzaSyDOxvc3ajTHyZLXcVPkqb7Nbeo3Tn8eFCs",
authDomain: "eegedu.firebaseapp.com",
databaseURL: "https://eegedu.firebaseio.com",
projectId: "eegedu",
storageBucket: "eegedu.appspot.com",
messagingSenderId: "987114688746",
appId: "1:987114688746:web:b13b68f24897ca97440793",
measurementId: "G-2HB7VKGKV9"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
var perf = firebase.performance();
firebase.analytics();
document.addEventListener("DOMContentLoaded", () => {
try {
firebase.app();
} catch (e) {
console.error(e);
console.log("Error loading the Firebase SDK, check the console.");
}
});
</script>
</body>
</html>
================================================
FILE: public/manifest.json
================================================
{
"short_name": "EEGEdu",
"name": "EEGedu Application",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: src/components/App/App.js
================================================
import React from "react";
import { PageSwitcher } from "../PageSwitcher/PageSwitcher";
import { AppProvider, Card, Page, Link } from "@shopify/polaris";
import enTranslations from "@shopify/polaris/locales/en.json";
import * as translations from "./translations/en.json";
export function App() {
return (
<AppProvider i18n={enTranslations}>
<Page title={translations.title} subtitle={translations.subtitle}>
<PageSwitcher />
<Card sectioned>
<p>{translations.footer}
A
<Link url="http://kylemathewson.com"> Ky</Link>
<Link url="http://korymathewson.com">Kor</Link>
<Link url="http://keyfer.ca">Key </Link>
Production.
<Link url="https://github.com/kylemath/EEGEdu/"> Github source code </Link>
</p>
</Card>
</Page>
</AppProvider>
);
}
================================================
FILE: src/components/App/translations/en.json
================================================
{
"title": "EEGEdu",
"subtitle": [
"Welcome to the EEGEdu live EEG tutorial. ",
"This tutorial will help you learn about how neurons produce electrical activity we can measure. ",
"By sticking electrodes on the head we can pick up these changes in electricity. ",
"The tutorial will walk you through the basics of EEG signal generation, data collection, and analysis with a focus on live control based on physiological signals. ",
"All demos are done in this browser. ",
"This tutorial is designed to be used with the Muse and the Muse 2 headbands from Interaxon. ",
"If you do not have one handy, there is an option to stream mock data as well. ",
"Muse with two auxillary ports made in 2014 will not work. ",
"This tutorial has been tested on Android Pixels (Mobile) and Mac OSX (laptop) with the latest chrome browser. ",
"The first step will be to turn on your Muse headband and click the connect button. ",
"This will open a screen and will list available Muse devices. ",
"Select the serial number written on your Muse. ",
"If you do not have a Muse headband you can click the Mock Data button to use simluated data. ",
"Then scroll down to see you live brain activity!"
],
"footer": "EEGEdu - An Interactive Electrophysiology Tutorial with the Muse brought to you by Mathewson Sons. "
}
================================================
FILE: src/components/PageSwitcher/PageSwitcher.js
================================================
import React, { useState, useCallback } from "react";
import { MuseClient } from "muse-js";
import { Select, Card, Stack, Button, ButtonGroup, Checkbox } from "@shopify/polaris";
import { mockMuseEEG } from "./utils/mockMuseEEG";
import * as translations from "./translations/en.json";
import * as generalTranslations from "./components/translations/en";
import { emptyAuxChannelData } 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";
import * as funAnimate from "./components/EEGEduAnimate/EEGEduAnimate";
import * as funSpectro from "./components/EEGEduSpectro/EEGEduSpectro";
import * as funAlpha from "./components/EEGEduAlpha/EEGEduAlpha";
import * as funSsvep from "./components/EEGEduSsvep/EEGEduSsvep";
import * as funEvoked from "./components/EEGEduEvoked/EEGEduEvoked";
import * as funPredict from "./components/EEGEduPredict/EEGEduPredict";
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;
const animate = translations.types.animate;
const spectro = translations.types.spectro;
const alpha = translations.types.alpha;
const ssvep = translations.types.ssvep;
const evoked = translations.types.evoked;
const predict = translations.types.predict;
export function PageSwitcher() {
// For auxEnable settings
const [checked, setChecked] = useState(false);
const handleChange = useCallback((newChecked) => setChecked(newChecked), []);
window.enableAux = checked;
if (window.enableAux) {
window.nchans = 5;
} else {
window.nchans = 4;
}
let showAux = true; // if it is even available to press (to prevent in some modules)
// data pulled out of multicast$
const [introData, setIntroData] = useState(emptyAuxChannelData)
const [heartRawData, setHeartRawData] = useState(emptyAuxChannelData);
const [heartSpectraData, setHeartSpectraData] = useState(emptyAuxChannelData);
const [rawData, setRawData] = useState(emptyAuxChannelData);
const [spectraData, setSpectraData] = useState(emptyAuxChannelData);
const [bandsData, setBandsData] = useState(emptyAuxChannelData);
const [animateData, setAnimateData] = useState(emptyAuxChannelData);
const [spectroData, setSpectroData] = useState(emptyAuxChannelData);
const [alphaData, setAlphaData] = useState(emptyAuxChannelData);
const [ssvepData, setSsvepData] = useState(emptyAuxChannelData);
const [evokedData, setEvokedData] = useState(emptyAuxChannelData);
const [predictData, setPredictData] = useState(emptyAuxChannelData);
// pipe settings
const [introSettings] = useState(funIntro.getSettings);
const [heartRawSettings] = useState(funHeartRaw.getSettings);
const [heartSpectraSettings, setHeartSpectraSettings] = useState(funHeartSpectra.getSettings);
const [rawSettings, setRawSettings] = useState(funRaw.getSettings);
const [spectraSettings, setSpectraSettings] = useState(funSpectra.getSettings);
const [bandsSettings, setBandsSettings] = useState(funBands.getSettings);
const [animateSettings, setAnimateSettings] = useState(funAnimate.getSettings);
const [spectroSettings, setSpectroSettings] = useState(funSpectro.getSettings);
const [alphaSettings, setAlphaSettings] = useState(funAlpha.getSettings);
const [ssvepSettings, setSsvepSettings] = useState(funSsvep.getSettings);
const [evokedSettings, setEvokedSettings] = useState(funEvoked.getSettings);
const [predictSettings, setPredictSettings] = useState(funPredict.getSettings);
// connection status
const [status, setStatus] = useState(generalTranslations.connect);
// for picking a new module
const [selected, setSelected] = useState(intro);
const handleSelectChange = useCallback(value => {
setSelected(value);
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();
if (window.subscriptionAnimate) window.subscriptionAnimate.unsubscribe();
if (window.subscriptionSpectro) window.subscriptionSpectro.unsubscribe();
if (window.subscriptionAlpha) window.subscriptionAlpha.unsubscribe();
if (window.subscriptionSsvep) window.subscriptionSsvep.unsubscribe();
if (window.subscriptionEvoked) window.subscriptionEvoked.unsubscribe();
if (window.subscriptionPredict) window.subscriptionPredict.unsubscribe();
subscriptionSetup(value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// for popup flag when recording
const [recordPop, setRecordPop] = useState(false);
const recordPopChange = useCallback(() => setRecordPop(!recordPop), [recordPop]);
// for popup flag when recording 2nd condition
const [recordTwoPop, setRecordTwoPop] = useState(false);
const recordTwoPopChange = useCallback(() => setRecordTwoPop(!recordTwoPop), [recordTwoPop]);
switch (selected) {
case intro:
showAux = false;
break
case heartRaw:
showAux = false;
break
case heartSpectra:
showAux = false;
break
case raw:
showAux = true;
break
case spectra:
showAux = true;
break
case bands:
showAux = true;
break
case animate:
showAux = false;
break
case spectro:
showAux = false;
break
case alpha:
showAux = true;
break
case ssvep:
showAux = true;
break
case evoked:
showAux = true;
break
case predict:
showAux = false;
break
default:
console.log("Error on showAux");
}
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 },
{ label: animate, value: animate },
{ label: spectro, value: spectro },
{ label: alpha, value: alpha },
{ label: ssvep, value: ssvep },
{ label: evoked, value: evoked },
{ label: predict, value: predict }
];
function buildPipes(value) {
funIntro.buildPipe(introSettings);
funHeartRaw.buildPipe(heartRawSettings);
funHeartSpectra.buildPipe(heartSpectraSettings);
funRaw.buildPipe(rawSettings);
funSpectra.buildPipe(spectraSettings);
funBands.buildPipe(bandsSettings);
funAnimate.buildPipe(animateSettings);
funSpectro.buildPipe(spectroSettings);
funAlpha.buildPipe(alphaSettings);
funSsvep.buildPipe(ssvepSettings);
funEvoked.buildPipe(evokedSettings);
funPredict.buildPipe(predictSettings);
}
function subscriptionSetup(value) {
switch (value) {
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;
case spectra:
funSpectra.setup(setSpectraData, spectraSettings);
break;
case bands:
funBands.setup(setBandsData, bandsSettings);
break;
case animate:
funAnimate.setup(setAnimateData, animateSettings);
break;
case spectro:
funSpectro.setup(setSpectroData, spectroSettings);
break;
case alpha:
funAlpha.setup(setAlphaData, alphaSettings);
break;
case ssvep:
funSsvep.setup(setSsvepData, ssvepSettings);
break;
case evoked:
funEvoked.setup(setEvokedData, evokedSettings);
break;
case predict:
funPredict.setup(setPredictData, predictSettings);
break;
default:
console.log(
"Error on handle Subscriptions. Couldn't switch to: " + value
);
}
}
async function connect() {
try {
if (window.debugWithMock) {
// Debug with Mock EEG Data
setStatus(generalTranslations.connectingMock);
window.source = {};
window.source.connectionStatus = {};
window.source.connectionStatus.value = true;
window.source.eegReadings$ = mockMuseEEG(256);
setStatus(generalTranslations.connectedMock);
} else {
// Connect with the Muse EEG Client
setStatus(generalTranslations.connecting);
window.source = new MuseClient();
window.source.enableAux = window.enableAux;
await window.source.connect();
await window.source.start();
window.source.eegReadings$ = window.source.eegReadings;
setStatus(generalTranslations.connected);
}
if (
window.source.connectionStatus.value === true &&
window.source.eegReadings$
) {
buildPipes(selected);
subscriptionSetup(selected);
}
} catch (err) {
setStatus(generalTranslations.connect);
console.log("Connection error: " + err);
}
}
function refreshPage(){
window.location.reload();
}
function pipeSettingsDisplay() {
switch(selected) {
case intro:
return null
case heartRaw:
return null
case heartSpectra:
return null
case raw:
return (
funRaw.renderSliders(setRawData, setRawSettings, status, rawSettings)
);
case spectra:
return (
funSpectra.renderSliders(setSpectraData, setSpectraSettings, status, spectraSettings)
);
case bands:
return (
funBands.renderSliders(setBandsData, setBandsSettings, status, bandsSettings)
);
case animate:
return (
funAnimate.renderSliders(setAnimateData, setAnimateSettings, status, animateSettings)
);
case spectro:
return (
funSpectro.renderSliders(setSpectroData, setSpectroSettings, status, spectroSettings)
);
case alpha:
return (
funAlpha.renderSliders(setAlphaData, setAlphaSettings, status, alphaSettings)
);
case ssvep:
return (
funSsvep.renderSliders(setSsvepData, setSsvepSettings, status, ssvepSettings)
);
case evoked:
return null
case predict:
return (
funPredict.renderSliders(setPredictData, setPredictSettings, status, predictSettings)
);
default: console.log('Error rendering settings display');
}
}
function renderModules() {
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:
return <funSpectra.renderModule data={spectraData} />;
case bands:
return <funBands.renderModule data={bandsData} />;
case animate:
return <funAnimate.renderModule data={animateData} />;
case spectro:
return <funSpectro.renderModule data={spectroData} />;
case alpha:
return <funAlpha.renderModule data={alphaData} />;
case ssvep:
return <funSsvep.renderModule data={ssvepData} />;
case evoked:
return <funEvoked.renderModule data={evokedData} />;
case predict:
return <funPredict.renderModule data={predictData} />;
default:
console.log("Error on renderCharts switch.");
}
}
function renderRecord() {
switch (selected) {
case intro:
return null
case heartRaw:
return (
funHeartRaw.renderRecord(recordPopChange, recordPop, status, heartRawSettings)
)
case heartSpectra:
return (
funHeartSpectra.renderRecord(recordPopChange, recordPop, status, heartSpectraSettings, setHeartSpectraSettings)
)
case raw:
return (
funRaw.renderRecord(recordPopChange, recordPop, status, rawSettings, setRawSettings)
)
case spectra:
return (
funSpectra.renderRecord(recordPopChange, recordPop, status, spectraSettings, setSpectraSettings)
)
case bands:
return (
funBands.renderRecord(recordPopChange, recordPop, status, bandsSettings, setBandsSettings)
)
case animate:
return null
case spectro:
return null
case alpha:
return (
funAlpha.renderRecord(recordPopChange, recordPop, status, alphaSettings, recordTwoPopChange, recordTwoPop, setAlphaSettings)
)
case ssvep:
return (
funSsvep.renderRecord(recordPopChange, recordPop, status, ssvepSettings, recordTwoPopChange, recordTwoPop, setSsvepSettings)
)
case evoked:
return (
funEvoked.renderRecord(recordPopChange, recordPop, status, evokedSettings, setEvokedSettings)
)
case predict:
return (
funPredict.renderRecord(recordPopChange, status)
)
default:
console.log("Error on renderRecord.");
}
}
// Render the entire page using above functions
return (
<React.Fragment>
<Card sectioned>
<Stack>
<ButtonGroup>
<Button
primary={status === generalTranslations.connect}
disabled={status !== generalTranslations.connect}
onClick={() => {
window.debugWithMock = false;
connect();
}}
>
{status}
</Button>
<Button
disabled={status !== generalTranslations.connect}
onClick={() => {
window.debugWithMock = true;
connect();
}}
>
{status === generalTranslations.connect ? generalTranslations.connectMock : status}
</Button>
<Button
destructive
onClick={refreshPage}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect}
>
{generalTranslations.disconnect}
</Button>
</ButtonGroup>
<Checkbox
label="Enable Muse Auxillary Channel"
checked={checked}
onChange={handleChange}
disabled={!showAux || status !== generalTranslations.connect}
/>
</Stack>
</Card>
<Card title={translations.title} sectioned>
<Select
label={""}
options={chartTypes}
onChange={handleSelectChange}
value={selected}
/>
</Card>
{pipeSettingsDisplay()}
{renderModules()}
{renderRecord()}
</React.Fragment>
);
}
================================================
FILE: src/components/PageSwitcher/components/EEGEduAlpha/EEGEduAlpha.js
================================================
import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal, Link } from "@shopify/polaris";
import { saveAs } from 'file-saver';
import { take, takeUntil } from "rxjs/operators";
import { Subject, timer } from "rxjs";
import { channelNames } from "muse-js";
import { Line } from "react-chartjs-2";
import { zipSamples } from "muse-js";
import YouTube from 'react-youtube'
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";
import P5Wrapper from 'react-p5-wrapper';
import sketchFixation from "./sketchFixation"
export function getSettings() {
return {
cutOffLow: 2,
cutOffHigh: 20,
interval: 100,
bins: 256,
sliceFFTLow: 1,
sliceFFTHigh: 30,
duration: 1024,
srate: 256,
name: 'Alpha',
secondsToSave: 60
}
};
const conds = ['Open', 'Closed'];
const thisRand = Math.floor(Math.random() * 2);
const firstType = conds[thisRand];
export function buildPipe(Settings) {
if (window.subscriptionAlpha) window.subscriptionAlpha.unsubscribe();
window.pipeAlpha$ = null;
window.multicastAlpha$ = null;
window.subscriptionAlpha = null;
// Build Pipe
window.pipeAlpha$ = zipSamples(window.source.eegReadings$).pipe(
bandpassFilter({
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
nbChannels: window.nchans }),
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.multicastAlpha$ = window.pipeAlpha$.pipe(
multicast(() => new Subject())
);
}
export function setup(setData, Settings) {
console.log("Subscribing to " + Settings.name);
if (window.multicastAlpha$) {
window.subscriptionAlpha = window.multicastAlpha$.subscribe(data => {
setData(alphaData => {
Object.values(alphaData).forEach((channel, index) => {
channel.datasets[0].data = data.psd[index];
channel.xLabels = data.freqs;
});
return {
ch0: alphaData.ch0,
ch1: alphaData.ch1,
ch2: alphaData.ch2,
ch3: alphaData.ch3,
ch4: alphaData.ch4
};
});
});
window.multicastAlpha$.connect();
console.log("Subscribed to " + Settings.name);
}
}
export function renderModule(channels) {
function renderCharts() {
return Object.values(channels.data).map((channel, index) => {
if (index === 0) {
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>
);
} else {
return null
}
});
}
return (
<Card title={specificTranslations.title}>
<Card.Section>
<Stack>
<TextContainer>
<p>{specificTranslations.description}</p>
</TextContainer>
</Stack>
</Card.Section>
<Card.Section title={'Previous Module'}>
<TextContainer>
<p> {
"Last module we recorded the frequency bands while people had their eyes open or closed for 10 seconds. Although we predicted there should be larger alpha power when the eyes are closed, are results showed the opposite effect:"
} </p>
</TextContainer>
<br />
<img
src={ require("./module5.png")}
alt="module5"
width="50%"
height="auto"
></img>
<br />
<br />
<TextContainer>
<p> {
"We pooled our estimates for alpha in eyes open vs eyes closed as a class, and computed the average and compared the results with a paired samples t-test. As you can see, even if we remove some of the outliers with extreme values that may have been artifacts, we still find that for most people we estimated larger alpha power when their eyes were open. Why could this be?"
}
<Link url="https://docs.google.com/spreadsheets/d/1d4oakU6ER_fDO-t2eHk1Wrj3F99eZYZGkjVXVXVwNQs/edit?usp=sharing"
external={true}
> You can access a read only copy of the spreadsheet we used to compute these group level effects here. </Link>
</p>
</TextContainer>
</Card.Section>
<Card.Section title={'Limitations'}>
<TextContainer>
<p> {
"We discussed in class that we had very little control over WHAT people were doing while their eyes were open. Some were looking around, some may have been blinking, some may have been reading their screen. There was a great deal of variability in the EEG signal due to differences in peoples behaviour during our task. As we have seen in previous modules, these kind of artifacts can create artificial power in most lower frequency bands. Because we were looking just at the alpha frequency band, we did not estimate the full range of differences across frequencies. "
} </p>
</TextContainer>
<br />
<TextContainer>
<p> {
"Furthermore, the short recording time (10 Seconds) also allows for less reliable measurement of the EEG signal. In addition, people may not have been prepared when the button was pressed to start their recording. "
} </p>
</TextContainer>
</Card.Section>
<Card.Section title={'New Experiment'}>
<TextContainer>
<p> {
"Based on all these ideas, in this module we are going to run a more controlled experiment comparing the EEG during eyes open and eyes closed conditions. Instead of just recording averages over frequency bands, we will record the whole spectra."
} </p>
</TextContainer>
<br />
<TextContainer>
<p> {
"This module, the recording sessions will be 60 seconds long. They will also begin with a 2 second delay after pressing the button. A window will pop up with a red fixation cross. In the eyes open condition participants should keep their eyes fixed on this red fixation cross the entire 60 seconds, and should try to minimize blinking as much as possible."
} </p>
</TextContainer>
<br />
<TextContainer>
<p> {
"Please DO NOT change any of the settings. The module will pick a random number to tell you which condition to run first, so that we can counterbalance the order. Keep track of this order in a lab notebook you will submit."
} </p>
</TextContainer>
<br />
</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, recordTwoPopChange, recordTwoPop, setSettings) {
function handleSecondsToSaveRangeSliderChange(value) {
setSettings(prevState => ({...prevState, secondsToSave: value}));
}
const recordDelay = 2000;
const opts = {
height: '195',
width: '320',
playerVars: { // https://developers.google.com/youtube/player_parameters
autoplay: false
}
};
return(
<Card title={'Record ' + Settings.name +' Data'} sectioned>
<Stack>
<TextContainer>
<p> {
"Each person will record two sessions, one with eyes open and one with eyes closed. The output files are identical to those we recorded in the spectra module 5. "
} </p>
</TextContainer>
<TextContainer>
<p> {
"The random number generator suggest you run the following condition first: " + firstType
} </p>
</TextContainer>
<br />
<ButtonGroup>
<RangeSlider
disabled={status === generalTranslations.connect}
min={2}
max={180}
label={'Recording Length: ' + Settings.secondsToSave + ' Seconds'}
value={Settings.secondsToSave}
onChange={handleSecondsToSaveRangeSliderChange}
/>
<Button
onClick={() => {
recordPopChange();
setTimeout(() => {
saveToCSV(Settings, "Closed");
}, recordDelay);
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect || recordPop}
>
{'Record Eyes Closed Data'}
</Button>
<Button
onClick={() => {
recordTwoPopChange();
setTimeout(() => {
saveToCSV(Settings, "Open");
}, recordDelay);
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect || recordTwoPop}
>
{'Record Eyes Open Data'}
</Button>
</ButtonGroup>
<TextContainer>
<p> {
"Instead of averaging over all electrodes, we will separately average the frontal and temporal electrodes to look for differences in different locations on the head. First you will each analyze your own data to summarize what you found. We will average the spectra values over the entire 60 seconds, and also over contralateral pairs of electrodes. You can watch a video showing you how to do this if you need a reminder here: "
} </p>
</TextContainer>
<YouTube
videoId="D9skUfAOstE"
opts={opts}
/>
<TextContainer>
<p> {
"Finally, we will paste the resulting average spectra in each condition, at each of the two electrode locations into a group spreadsheet that we will use for our final assignment paper in the class. To clarify, each participant will make two recordings 1) Eyes open, and 2) Eyes closed. For each, the analysis will produce two arrays of numbers (spectra), one for Frontal electrodes, and one for posterior electrodes. Therefore there are four different tabs in the following group data log:"
} </p>
</TextContainer>
<Link url="https://docs.google.com/spreadsheets/d/1Ip8Xitp548DVXikZhL55Ll-Vgg9UAFU0-N40_3-e8sw/edit?usp=sharing"
external={true}
> Group Data Log (4 tabs) </Link>
<Modal
open={recordPop}
onClose={recordPopChange}
title="Recording Eye Closed Data"
>
<Modal.Section>
<Card.Section>
<P5Wrapper sketch={sketchFixation} />
</Card.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>
<Modal
open={recordTwoPop}
onClose={recordTwoPopChange}
title="Recording Eyes Open Data"
>
<Modal.Section>
<Card.Section>
<P5Wrapper sketch={sketchFixation} />
</Card.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, condition) {
console.log('Saving ' + Settings.secondsToSave + ' seconds...');
var localObservable$ = null;
const dataToSave = [];
console.log('making ' + Settings.name + ' headers')
// take one sample from selected observable object for headers
localObservable$ = window.multicastAlpha$.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"
);
}
});
// setup timer
const timer$ = timer(Settings.secondsToSave * 1000)
// put selected observable object into local and start taking samples
localObservable$ = window.multicastAlpha$.pipe(
takeUntil(timer$)
);
// 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 + "_" + condition + "_Recording_" + Date.now() + ".csv");
console.log('Completed');
}
});
}
================================================
FILE: src/components/PageSwitcher/components/EEGEduAlpha/sketchFixation.js
================================================
export default function sketchFixation (p) {
p.setup = function () {
p.createCanvas(300, 300);
};
p.windowResized = function() {
p.createCanvas(300, 300);
}
p.mousePressed = function () {
p.background(256);
}
p.draw = function () {
p.background(255);
p.fill(255,0,0);
p.text("+", p.width/2, p.height/2);
}
};
================================================
FILE: src/components/PageSwitcher/components/EEGEduAlpha/translations/en.json
================================================
{
"title": "Eyes open vs. Eyes Closed Alpha Experiment",
"description": [
"In the next demo we run our first experiment, comparing the spectra when eyes are open vs when they are closed. ",
"In this module you will first adjust the muse so the signal is good, and then record two sessions of data. ",
"In one, you will keep you eyes open and stare at a single point on the screen that pops up. ",
"In the other, you will close your eyes and the recording will begin. ",
"Alpha oscillations are found to be larger when the eyes are closed than when they are open."
],
"xlabel": "Frequency (Hz)",
"ylabel": "Power (\u03BCV\u00B2)"
}
================================================
FILE: src/components/PageSwitcher/components/EEGEduAnimate/EEGEduAnimate.js
================================================
import React, { useState, useCallback } from "react";
import { catchError, multicast } from "rxjs/operators";
import { Card, Stack, TextContainer, RangeSlider, Select, Link} from "@shopify/polaris";
import { Subject } from "rxjs";
import { zipSamples } from "muse-js";
import {
bandpassFilter,
epoch,
fft,
powerByBand
} from "@neurosity/pipes";
import { chartStyles } from "../chartOptions";
import * as generalTranslations from "../translations/en";
import * as specificTranslations from "./translations/en";
import { bandLabels } from "../../utils/chartUtils";
import sketchBands from './sketchBands'
import sketchTone from './sketchTone'
import sketchCube from './sketchCube'
import sketchFlock from './sketchFlock'
import sketchDraw from './sketchDraw'
import sketchFlock3D from './sketchFlock3D'
import P5Wrapper from 'react-p5-wrapper';
export function getSettings () {
return {
cutOffLow: 2,
cutOffHigh: 20,
interval: 16,
bins: 256,
duration: 128,
srate: 256,
name: 'Animate'
}
};
export function buildPipe(Settings) {
if (window.subscriptionAnimate) window.subscriptionAnimate.unsubscribe();
window.pipeAnimate$ = null;
window.multicastAnimate$ = null;
window.subscriptionAnimate = null;
// Build Pipe
window.pipeAnimate$ = zipSamples(window.source.eegReadings$).pipe(
bandpassFilter({
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
nbChannels: window.nchans }),
epoch({
duration: Settings.duration,
interval: Settings.interval,
samplingRate: Settings.srate
}),
fft({ bins: Settings.bins }),
powerByBand(),
catchError(err => {
console.log(err);
})
);
window.multicastAnimate$ = window.pipeAnimate$.pipe(
multicast(() => new Subject())
);
}
export function setup(setData, Settings) {
console.log("Subscribing to " + Settings.name);
if (window.multicastAnimate$) {
window.subscriptionAnimate = window.multicastAnimate$.subscribe(data => {
setData(animateData => {
Object.values(animateData).forEach((channel, index) => {
channel.datasets[0].data = [
data.delta[index],
data.theta[index],
data.alpha[index],
data.beta[index],
data.gamma[index]
];
channel.xLabels = bandLabels;
});
return {
ch0: animateData.ch0,
ch1: animateData.ch1,
ch2: animateData.ch2,
ch3: animateData.ch3,
ch4: animateData.ch4
};
});
});
window.multicastAnimate$.connect();
console.log("Subscribed to " + Settings.name);
}
}
export function renderModule(channels) {
function RenderCharts() {
const bands = '3D Frequency Bands';
const tone = 'Play simple music with your frequency bands';
const cube = 'Control a Cube with your Alpha Power';
const flock = 'Control a Flock with Alpha and Beta';
const draw = 'Draw a picture with Alpha and Beta';
const flock3d = 'Control a 3d Flock with Alpha, Beta, and Theta';
const chartTypes = [
{ label: bands, value: bands },
{ label: tone, value: tone },
{ label: cube, value: cube },
{ label: flock, value: flock },
{ label: draw, value: draw },
{ label: flock3d, value: flock3d }
];
// for picking a new animation
const [selectedAnimation, setSelectedAnimation] = useState(bands);
const handleSelectChangeAnimation = useCallback(value => {
setSelectedAnimation(value);
console.log("Switching to: " + value);
}, []);
return Object.values(channels.data).map((channel, index) => {
if (channel.datasets[0].data) {
if (index === 1) {
// console.log( channel.datasets[0].data[2])
window.delta = channel.datasets[0].data[0];
window.theta = channel.datasets[0].data[1];
window.alpha = channel.datasets[0].data[2];
window.beta = channel.datasets[0].data[3];
window.gamma = channel.datasets[0].data[4];
}
}
let thisSketch = sketchTone;
switch (selectedAnimation) {
case bands:
thisSketch = sketchBands;
break
case tone:
thisSketch = sketchTone;
break
case cube:
thisSketch = sketchCube;
break
case flock:
thisSketch = sketchFlock;
break
case draw:
thisSketch = sketchDraw;
break
case flock3d:
thisSketch = sketchFlock3D;
break
default: console.log("Error on switch to " + selectedAnimation)
}
//only left frontal channel
if (index === 1) {
return (
<React.Fragment key={'dum'}>
<Card.Section
title={"Choice of Sketch"}
>
<Select
label={""}
options={chartTypes}
onChange={handleSelectChangeAnimation}
value={selectedAnimation}
/>
</Card.Section>
<Card.Section>
<P5Wrapper sketch={thisSketch}
delta={window.delta}
theta={window.theta}
alpha={window.alpha}
beta={window.beta}
gamma={window.gamma}
/>
</Card.Section>
</React.Fragment>
);
} else {
return null
}
});
}
return (
<Card title={specificTranslations.title}>
<Card.Section>
<Stack>
<TextContainer>
<p>{specificTranslations.description}</p>
</TextContainer>
<img
src={ require("./electrodediagram2.png")}
alt="F7Electrode"
width="25%"
height="auto"
></img>
<Link url="https://p5js.org/learn/interactivity.html"
external={true}>
Link to learn more about making your own easy P5.js animations </Link>
<br />
<Link url="https://p5js.org/examples/"
external={true}>
Examples of other P5.js animations to get started </Link>
<TextContainer>
<p>{[
"We are working on allowing for people to make their own animations with a p5.js editor. ",
"For now if you want us to make a new one make a github issue with your p5.js animation and describe your idea"
]}</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={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}
/>
</Card>
)
}
================================================
FILE: src/components/PageSwitcher/components/EEGEduAnimate/sketchBands.js
================================================
export default function sketchBands (p) {
let delta = 0;
let theta = 0;
let alpha = 0;
let beta = 0;
let gamma = 0;
p.setup = function () {
p.createCanvas(p.windowWidth*.5, p.windowWidth*.5, p.WEBGL);
};
p.windowResized = function() {
p.resizeCanvas(p.windowWidth*.5, p.windowWidth*.5);
}
p.myCustomRedrawAccordingToNewPropsHandler = function (props) {
delta = props.delta;
theta = props.theta;
alpha = props.alpha;
beta = props.beta;
gamma = props.gamma;
};
p.draw = function () {
let unit = p.width/5;
p.background(256);
p.ambientMaterial(250);
p.noStroke();
let locX = p.mouseX - p.height / 2;
let locY = p.mouseY - p.width / 2;
p.ambientLight(60, 60, 60);
p.pointLight(255, 255, 255, locX, locY, 100);
p.push();
p.fill(255,0,0);
p.translate(-unit,0);
p.rotateY(50);
p.rotateX(50);
p.box(unit/2, delta* 10, unit);
p.pop();
p.push();
p.fill(0,255,0);
p.translate(-unit/2,0);
p.rotateY(50);
p.rotateX(50);
p.box(unit/2, theta* 10, unit);
p.pop();
p.push();
p.fill(0,0,255);
p.translate(0,0);
p.rotateY(50);
p.rotateX(50);
p.box(unit/2, alpha* 10, unit);
p.pop();
p.push();
p.fill(0,128, 128);
p.translate(unit/2,0);
p.rotateY(50);
p.rotateX(50);
p.box(unit/2, beta* 10, unit);
p.pop();
p.push();
p.fill(128,0,128);
p.translate(unit,0);
p.rotateY(50);
p.rotateX(50);
p.box(unit/2, gamma* 10, unit);
p.pop();
};
};
================================================
FILE: src/components/PageSwitcher/components/EEGEduAnimate/sketchCube.js
================================================
export default function sketchCube (p) {
let delta = 0;
// let theta = 0;
// let alpha = 0;
// let beta = 0;
// let gamma = 0;
let rotation = 0;
p.setup = function () {
p.createCanvas(p.windowWidth*.6, 800, p.WEBGL);
};
p.windowResized = function() {
p.resizeCanvas(p.windowWidth*.6, 800);
}
p.myCustomRedrawAccordingToNewPropsHandler = function (props) {
delta = Math.floor(props.delta);
// theta = props.theta;
// alpha = props.alpha;
// beta = props.beta;
// gamma = props.gamma;
if (props.alpha){
rotation = props.alpha * Math.PI / 180;
}
};
p.draw = function () {
p.background(256);
p.normalMaterial();
p.noStroke();
p.push();
p.rotateX(25 + p.frameCount/10);
p.rotateY(rotation);
p.box(delta*10);
p.pop();
};
};
================================================
FILE: src/components/PageSwitcher/components/EEGEduAnimate/sketchDraw.js
================================================
export default function sketchDraw (p) {
let delta = 0;
let theta = 0;
let alpha = 0;
let beta = 0;
let gamma = 0;
let xVar = 0;
let yVar = 0;
let brushWidth = 50;
p.setup = function () {
p.createCanvas(p.windowWidth*.6, 500);
};
p.windowResized = function() {
p.createCanvas(p.windowWidth*.6, 500);
}
p.myCustomRedrawAccordingToNewPropsHandler = function (props) {
delta = Math.floor((props.delta/20) * 255);
theta = Math.floor((props.theta/10) * 255);
alpha = Math.floor((props.alpha/5) * p.width);
beta = Math.floor((props.beta/2) * p.height);
gamma = Math.floor((props.gamma/2) * 255);
xVar = alpha;
yVar = beta;
if (xVar > p.width) {
xVar = p.width-brushWidth/2;
}
if (yVar > p.height) {
yVar = p.height-brushWidth/2;
}
// console.log(xVar)
// console.log(yVar)
};
p.mousePressed = function () {
p.background(256);
}
p.draw = function () {
p.fill(theta, delta, gamma, 20);
p.noStroke();
p.ellipse(xVar, yVar, brushWidth);
}
};
================================================
FILE: src/components/PageSwitcher/components/EEGEduAnimate/sketchFlock.js
================================================
import p5 from "p5";
import "p5/lib/addons/p5.sound";
export default function sketchFlock (p) {
let alpha = 0;
let beta = 0;
let xVar = 0;
let yVar = 0;
var flock;
p.setup = function () {
p.createCanvas(p.windowWidth*.6, 500);
flock = new p.Flock();
for (var i = 0; i < 150; i++) {
var b = new p.Boid(p.width / 2, p.height / 2);
flock.addBoid(b)
}
};
p.windowResized = function() {
p.createCanvas(p.windowWidth*.6, 500);
}
p.myCustomRedrawAccordingToNewPropsHandler = function (props) {
alpha = Math.floor((props.alpha/5) * p.width);
beta = Math.floor((props.beta/2) * p.height);
xVar = alpha;
yVar = beta;
if (xVar > p.width) {
xVar = p.width-5;
}
if (yVar > p.height) {
yVar = p.height-5;
}
// console.log(xVar)
// console.log(yVar)
};
p.draw = function () {
p.background(255);
p.fill(255,0,0)
p.push();
p.translate(xVar, yVar);
p.ellipse(0,0,10,10);
p.pop();
flock.run();
}
p.mouseDragged = function () {
flock.addBoid(new p.Boid(p.mouseX, p.mouseY));
}
p.Flock = function() {
this.boids = []; //Initialize the array
}
p.Flock.prototype.addBoid = function(b) {
this.boids.push(b);
}
p.Flock.prototype.run = function() {
for (var i=0; i < this.boids.length; i++) {
this.boids[i].run(this.boids);
}
}
p.Boid = function(x, y) {
this.acceleration = p.createVector(0, 0);
this.velocity = p.createVector(p.random(-1, 1), p.random(-1, 1));
this.position = p.createVector(x, y);
this.r = 2.0; //Size of object Boid
this.maxspeed = 5; // Maximum speed
this.maxforce = 0.1; // Maximum steer ing force
}
p.Boid.prototype.run = function(boids) {
this.flock(boids);
this.update();
this.borders();
this.render();
};
p.Boid.prototype.applyForce = function(force) {
// We could add mass here if we want A = F / M
this.acceleration.add(force);
};
p.Boid.prototype.flock = function(boids) {
var sep = this.separate(boids);
var ali = this.align(boids);
var coh = this.cohesion(boids);
var mows = this.mouuse(boids);
sep.mult(1.5);
ali.mult(1.0);
coh.mult(1.0);
mows.mult(3.0);
this.applyForce(sep);
this.applyForce(ali);
this.applyForce(coh);
this.applyForce(mows);
};
p.Boid.prototype.update = function() {
this.velocity.add(this.acceleration);
this.velocity.limit(this.maxspeed);
this.position.add(this.velocity);
this.acceleration.mult(0);
};
p.Boid.prototype.seek = function(target) {
var desired = p5.Vector.sub(target, this.position);
desired.normalize();
desired.mult(this.maxspeed);
var steer = p5.Vector.sub(desired, this.velocity);
steer.limit(this.maxforce);
return steer;
};
p.Boid.prototype.render = function() {
// Draw a triangle rotated in the direction of velocity
var theta = this.velocity.heading() + p.radians(90);
p.fill(0);
p.noStroke();
p.push();
p.translate(this.position.x, this.position.y);
// p.ellipse(0,0,5,5);
p.rotate(theta);
p.beginShape();
p.vertex(0, -this.r * 2);
p.vertex(-this.r, this.r * 2);
p.vertex(this.r, this.r * 2);
p.endShape(p.CLOSE);
p.pop();
};
p.Boid.prototype.borders = function() {
if (this.position.x < -this.r) this.position.x = p.width + this.r;
if (this.position.y < -this.r) this.position.y = p.height + this.r;
if (this.position.x > p.width + this.r) this.position.x = -this.r;
if (this.position.y > p.height + this.r) this.position.y = -this.r;
};
p.Boid.prototype.separate = function(boids) {
var desiredseparation = 25.0;
var steer = p.createVector(0, 0);
var count = 0;
// For every boid in the system, check if it's too close
for (var i = 0; i < boids.length; i++) {
var d = p5.Vector.dist(this.position, boids[i].position);
// If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself)
if ((d > 0) && (d < desiredseparation)) {
// Calculate vector pointing away from neighbor
var diff = p5.Vector.sub(this.position, boids[i].position);
diff.normalize();
diff.div(d); // Weight by distance
steer.add(diff);
count++; // Keep track of how many
}
}
if (count > 0) {
steer.div(count);
}
if (steer.mag() > 0) {
steer.normalize();
steer.mult(this.maxspeed);
steer.sub(this.velocity);
steer.limit(this.maxforce);
}
return steer;
};
// Alignment
// For every nearby boid in the system, calculate the average velocity
p.Boid.prototype.align = function(boids) {
var neighbordist = 100;
var sum = p.createVector(0, 0);
var count = 0;
for (var i = 0; i < boids.length; i++) {
var d = p5.Vector.dist(this.position, boids[i].position);
if ((d > 0) && (d < neighbordist)) {
sum.add(boids[i].velocity);
count++;
}
}
if (count > 0) {
sum.div(count);
sum.normalize();
sum.mult(this.maxspeed);
var steer = p5.Vector.sub(sum, this.velocity);
steer.limit(this.maxforce);
return steer;
} else {
return p.createVector(0, 0);
}
};
// Cohesion
// For the average location (i.e. center) of all nearby boids, calculate steering vector towards that location
p.Boid.prototype.cohesion = function(boids) {
var neighbordist = 100;
var sum = p.createVector(0, 0); // Start with empty vector to accumulate all locations
var count = 0;
for (var i = 0; i < boids.length; i++) {
var d = p5.Vector.dist(this.position, boids[i].position);
if ((d > 0) && (d < neighbordist)) {
sum.add(boids[i].position); // Add location
count++;
}
}
if (count > 0) {
sum.div(count);
return this.seek(sum); // Steer towards the location
} else {
return p.createVector(0, 0);
}
};
p.Boid.prototype.mouuse = function(boids) {
var neighbordist = 500;
var m = p.createVector(xVar, yVar);
var d = p5.Vector.dist(this.position, m);
if ((d > 0) && (d < neighbordist)) {
return this.seek(m); // Steer towards the mouse location
} else {
return p.createVector(0, 0);
}
};
};
================================================
FILE: src/components/PageSwitcher/components/EEGEduAnimate/sketchFlock3D.js
================================================
import p5 from 'p5';
export default function sketchFlock3D (p) {
const flock = []; // Array of boids
let depth = 800; // The Z location of the boid tend to stay between +depth/2 and -depth/2
let gap = 300; // Boids can go further than the edges, this further distance is the gap
let quadTree; // A quad tree to minimize the cost of distance calculation
let useQuadTree = true; // Toogle the use of a quad tree
let showPerceptionRadius = false; // Toogle vizualization of perception radius
let boidsSlider, perceptionSlider, alignmentSlider, cohesionSlider, separationSlider; // Sliders
let boidsP, perceptionP, alignmentP, cohesionP, separationP; // Paragraphs
let startingBoids = 50; // Amount of boid at the start of the sketch
let startingPerception = 90; // Perception radius at the start of the sketch
let t = 0; // Counts the frame from the time boids go out of the middle of space
let theta = 0;
let alpha = 0;
let beta = 0;
let xVar = 0;
let yVar = 0;
let zVar = 0;
// SETUP FUNCTION ---------------------------------------------------
// Make the canvas, declare some variables, create the DOM elements and the initial boid population
p.setup = function () {
// Declaration of a canvas to allow canvas download
p.createCanvas(p.windowWidth*.5, p.windowWidth*.5, p.WEBGL); // You can change the resolution here
// Declaration of depth (z axis), unit vectors, and the camera
p.depth = p.height;
let cameraX = 1000 / 600 * p.width;
let cameraY = -800 / 600 * p.height;
let cameraZ = -200 / 500 * p.depth;
p.camera(cameraX, cameraY, cameraZ, 0, 0, 0, 0, 0, 1);
// Create the DOM elements: sliders and paragraphs
createDOMs();
// Create an initial population of 100 boids
for (let i = 0; i < boidsSlider.value(); i++) {
pushRandomBoid();
}
}
p.windowResized = function() {
p.createCanvas(p.windowWidth*.5, p.windowWidth*.5);
p.depth = p.height;
let cameraX = 1000 / 600 * p.width;
let cameraY = -800 / 600 * p.height;
let cameraZ = -200 / 500 * p.depth;
p.camera(cameraX, cameraY, cameraZ, 0, 0, 0, 0, 0, 1);
}
p.myCustomRedrawAccordingToNewPropsHandler = function (props) {
theta = Math.floor((props.theta/10) * p.width);
alpha = Math.floor((props.alpha/10) * p.height);
beta = Math.floor((props.beta/10) * p.depth);
xVar = theta-(p.width/2)+200;
yVar = alpha-(p.height/2)+200;
zVar = beta-(p.depth/2)+200;
if (Math.abs(xVar) > p.width/2) {
xVar = Math.sign(xVar) * (p.width/2);
}
if (Math.abs(yVar) > p.height/2) {
yVar = Math.sign(yVar) * (p.height/2);
}
if (Math.abs(zVar) > p.depth/2) {
zVar = Math.sign(zVar) * (p.depth/2);
}
// console.log(xVar + ' ' + yVar + ' ' + zVar)
};
// DRAW FUNCTION ---------------------------------------------------
p.draw = function () {
// Background and lightning
p.background(200);
//drag to move the world.
p.orbitControl();
p.directionalLight(150, 150, 150, 1, 1, 0);
p.ambientLight(150);
// Draw the corners of a box showing the space where boids can fly
p.stroke(80);
p.strokeWeight(8);
p.noFill();
p.box(p.width + gap/2, p.height + gap/2, p.depth + gap/2);
p.noStroke();
p.fill(255);
p.ambientMaterial(0, 0, 255);
p.push()
p.translate(xVar, yVar, zVar);
p.sphere(5); // A sphere where the boid is
p.pop();
// Make the quad tree
let boundary = new Cube(0, 0, 0, p.width + 2 * gap, p.height + 2 * gap, p.depth + 2 * gap);
quadTree = new QuadTree(boundary, 4);
for (let boid of flock) {
quadTree.insert(boid);
}
// Each boid determines its acceleration for the next frame
for (let boid of flock) {
boid.flock(flock, quadTree);
}
// Each boid updates its position and velocity, and is displayed on screen
for (let boid of flock) {
boid.update(gap);
boid.show();
}
// Adjust the amount of boids on screen according to the slider value
let maxBoids = boidsSlider.value();
let difference = flock.length - maxBoids;
if (difference < 0) {
for (let i = 0; i < -difference; i++) {
pushRandomBoid(); // Add boids if there are less boids than the slider value
}
} else if (difference > 0) {
for (let i = 0; i < difference; i++) {
flock.pop(); // Remove boids if there are more boids than the slider value
}
}
// Update the DOM elements
boidsP.html(`Boids: ${boidsSlider.value()}`);
perceptionP.html(`Perception: ${perceptionSlider.value()}`);
alignmentP.html(`Alignment: ${alignmentSlider.value()}`);
cohesionP.html(`Cohesion: ${cohesionSlider.value()}`);
separationP.html(`Separation: ${separationSlider.value()}`);
t++; // t counts the number of frames, it is used to not have cohesion in the first 40 frames
}
// Create the DOM elements
function createDOMs() {
// Create the paragraphs and sliders
boidsP = p.createP('Boids');
perceptionP = p.createP('Perception');
alignmentP = p.createP('Alignment');
cohesionP = p.createP('Cohesion');
separationP = p.createP('Separation');
if (p.windowWidth * p.windowHeight > 1200 * 1200) startingPerception = 150; // Larger perception on a larger screen
boidsSlider = p.createSlider(1, 500, startingBoids, 1);
perceptionSlider = p.createSlider(0, 1000, startingPerception, 1);
alignmentSlider = p.createSlider(0, 5, 0.2, 0.1);
cohesionSlider = p.createSlider(0, 5, 0.3, 0.1);
separationSlider = p.createSlider(0, 5, 0.7, 0.1);
// Position the DOM elements on the top left corner
let DOMoffset = 1050; // Place the DOM elements underneath the canvas when we want to download the canvas
let DOMgap = 5; // Gap between the DOM elements
let leftGap = 200;
boidsSlider.position( leftGap + DOMgap, DOMoffset + boidsSlider.height * 0 + 1 * DOMgap);
perceptionSlider.position(leftGap + DOMgap, DOMoffset + boidsSlider.height * 1 + 2 * DOMgap);
alignmentSlider.position( leftGap + DOMgap, DOMoffset + boidsSlider.height * 2 + 3 * DOMgap);
cohesionSlider.position( leftGap + DOMgap, DOMoffset + boidsSlider.height * 3 + 4 * DOMgap);
separationSlider.position(leftGap + DOMgap, DOMoffset + boidsSlider.height * 4 + 5 * DOMgap);
boidsP.position( leftGap + boidsSlider.width + DOMgap * 2, DOMoffset + boidsSlider.height * 0 + 0 * DOMgap + 2);
perceptionP.position( leftGap + boidsSlider.width + DOMgap * 2, DOMoffset + boidsSlider.height * 1 + 1 * DOMgap + 2);
alignmentP.position( leftGap + boidsSlider.width + DOMgap * 2, DOMoffset + boidsSlider.height * 2 + 2 * DOMgap + 2);
cohesionP.position( leftGap + boidsSlider.width + DOMgap * 2, DOMoffset + boidsSlider.height * 3 + 3 * DOMgap + 2);
separationP.position( leftGap + boidsSlider.width + DOMgap * 2, DOMoffset + boidsSlider.height * 4 + 4 * DOMgap + 2);
}
// Make a new boid
function pushRandomBoid() {
//let pos = createVector(random(width), random(height), random(-depth/2, depth/2)); // Uncomment and comment next line to create boids at random position
let pos = p.createVector(0, 0, 0); // Create a boid at the center of space
let vel = p5.Vector.random3D().mult(p.random(0.5, 3)); // Give a random velocity
let boid = new Boid(pos, vel); // Create a new boid
flock.push(boid); // Add the new boid to the flock
}
///---
///---
///---
// Boid class with flocking behavior
class Boid {
constructor(pos, vel) {
this.pos = pos; // Position
this.vel = vel; // Velocity
this.acc = p.createVector(0, 0, 0); // Acceleration
this.maxForce = 1; // Maximum steering force for alignment, cohesion, separation
this.maxSpeed = 10; // Desired velocity for the steering behaviors
this.r = 255; // red color of the boid
this.g = p.floor(p.random(50, 120)); // green color of the boid
this.b = p.floor(p.random(50, 120)); // blue color of the boid
}
// Alignment rule
// Steering to average neighbors velocity
alignment(neighbors) {
let steering = p.createVector();
for (let other of neighbors) steering.add(other.vel); // Sum of neighbor velocities
if (neighbors.length > 0) {
steering.div(neighbors.length); // Average neighbors velocity
steering.setMag(this.maxSpeed); // Desired velocity
steering.sub(this.vel); // Actual steering
steering.limit(this.maxForce); // Steering limited to maxForce
}
return steering;
}
// Cohesion rule
// Steering to the average neighbors position
cohesion(neighbors) {
let steering = p.createVector();
for (let other of neighbors) steering.add(other.pos); // Sum of neighbor positions
if (neighbors.length > 0) {
steering.div(neighbors.length); // Average neighbors position
steering.sub(this.pos); // Orientation of the desired velocity
steering.setMag(this.maxSpeed); // Desired velocity
steering.sub(this.vel); // Actual steering
steering.limit(this.maxForce); // Steering limited to maxForce
}
return steering;
}
// Separation rule
// Steering to avoid proximity of the neighbors
separation(neighbors) {
let steering = p.createVector();
for (let other of neighbors) {
let diff = p5.Vector.sub(this.pos, other.pos); // Vector from other boid to this boid
let d = p.max(other.distance, 0.01); // Distance between other boid and this boid
steering.add(diff.div(d)); // Magnitude inversely proportional to the distance
}
if (neighbors.length > 0) {
steering.div(neighbors.length); // Orientation of the desired velocity
steering.setMag(this.maxSpeed); // Desired velocity
steering.sub(this.vel); // Actual steering
steering.limit(this.maxForce); // Steering limited to maxForce
}
return steering;
}
// Application of the rules
flock(boids, quadTree) {
// Go to the middle if goMiddle is true
// Create a large force towards the middle, apply it to the boid, and "return" to not apply other forces
let force = p.createVector(xVar-this.pos.x, yVar-this.pos.y, zVar-this.pos.z);
force.setMag(this.maxForce);
this.acc.add(force);
let radius = perceptionSlider.value(); // Max distance of a neighbor
let neighbors = [];
if (useQuadTree === true) {
// VERSION WITH QUADTREE
// Make an array of neighbors, i.e. all boids closer than the perception radius
// The array will be passed to the different flocking behaviors
let range = new Cube(this.pos.x, this.pos.y, this.pos.z, radius, radius, radius);
let maybeNeighbors = quadTree.query(range);
for (let other of maybeNeighbors) {
let distance = this.pos.dist(other.pos);
if (other !== this && distance < radius) {
other.distance = distance; // Record the distance so it can be used later
neighbors.push(other); // Put this neighbor in the "neighbors" array
}
}
} else {
// VERSION WITHOUT QUADTREE
// Make an array of neighbors, i.e. all boids closer than the perception radius
// The array will be passed to the different flocking behaviors
for (let other of boids) {
let distance = this.pos.dist(other.pos);
if (other !== this && distance < radius) {
other.distance = distance; // Record the distance so it can be used later
neighbors.push(other); // Put this neighbor in the "neighbors" array
}
}
}
// Calculate the force of alignments and apply it to the boid
let alignment = this.alignment(neighbors);
alignment.mult(alignmentSlider.value());
this.acc.add(alignment);
// Calculate the force of cohesion and apply it to the boid
if (t > 2) { // No cohesion in the first 40 frames
let cohesion = this.cohesion(neighbors);
cohesion.mult(cohesionSlider.value());
this.acc.add(cohesion);
}
// Calculate the force of separation and apply it to the boid
let separation = this.separation(neighbors);
separation.mult(separationSlider.value());
this.acc.add(separation);
// If the boid is flies too high or too low, apply another force to make it fly around the middle of space's depth
if (this.pos.z < -depth/8 || this.pos.z > depth/8) {
let force = p.createVector(0, 0, -this.pos.z / depth * this.maxForce * 2);
this.acc.add(force);
}
// If the boid has no neighbor, apply random forces so it can go find other boids
if (neighbors.length === 0) {
let force = p5.Vector.random3D().mult(this.maxForce/4);
force.z = 0; // Only go find other in an XY plane
this.acc.add(force);
}
}
// Update position, velocity, and acceleration
update(gap) {
// Apply physics
this.pos.add(this.vel);
this.vel.add(this.acc);
this.vel.mult(0.999); // Some friction
this.vel.limit(this.maxSpeed);
this.acc.mult(0);
// Teleport to opposite side if the boid goes further than a side of space (X and Y axis)
// Except for the Z axis, as there is already a force keeping the boid from getting too far
if (this.pos.x > p.width/2 + gap) this.pos.x -= p.width + 1.7 * gap;
if (this.pos.x < -(p.width/2 + gap)) this.pos.x += p.width + 1.7 * gap;
if (this.pos.y > p.height/2 + gap) this.pos.y -= p.height + 1.7 * gap;
if (this.pos.y < -(p.height/2 + gap)) this.pos.y += p.eight + 1.7 * gap;
}
// Show the boid on screen
show() {
p.noStroke();
p.fill(255);
p.ambientMaterial(this.r, this.g, this.b);
p.push()
p.translate(this.pos.x, this.pos.y, this.pos.z);
p.sphere(10); // A sphere where the boid is
let arrow = p.createVector(this.vel.x, this.vel.y, this.vel.z).setMag(10);
p.translate(arrow.x, arrow.y, arrow.z);
p.sphere(5); // Another sphere, smaller, in the direction of the boid's velocity
p.pop();
// Show perception radius, all circles are drawn at z = 0
if (showPerceptionRadius) {
p.stroke(255, 255, 255, 100);
p.noFill();
p.strokeWeight(1);
let perception = perceptionSlider.value() * 2;
p.push();
p.translate(0,0,this.pos.z)
p.ellipse(this.pos.x, this.pos.y, perception, perception);
p.pop();
}
}
}
///
///
///
// This file contains the QuadTree class
// as well as the Cube classe used by the QuadTree
// Cube --------------------------------------------------
// A cube delimiting the volume of a quad tree
// or the volume used for asking boids from a quad tree
class Cube {
constructor(x, y, z, w, h, d) {
this.x = x;
this.y = y;
this.z = z;
this.w = w;
this.h = h;
this.d = d;
this.xMin = x - w;
this.xMax = x + w;
this.yMin = y - h;
this.yMax = y + h;
this.zMin = z - d;
this.zMax = z + d;
}
// Checks if a boid is inside the cube
contains(boid) {
let pos = boid.pos;
return (pos.x >= this.xMin && pos.x <= this.xMax &&
pos.y >= this.yMin && pos.y <= this.yMax &&
pos.z >= this.zMin && pos.z <= this.zMax);
}
// Check if two cubes intersect
intersects(range) {
return !(this.xMax < range.xMin || this.xMin > range.xMax ||
this.yMax < range.yMin || this.yMin > range.yMax ||
this.zMax < range.zMin || this.zMin > range.zMax);
}
}
// QUAD TREE --------------------------------------------------
// The quad tree stores points in a tree structure
// to minimize the cost of distance calculation
class QuadTree {
constructor(boundary, capacity) {
this.boundary = boundary; // cube giving the borders of the quad tree
this.capacity = capacity; // Maximum amount of points that can be stored in the quad tree
this.boids = []; // Array storing the boids in the quad tree
this.divided = false; // True when the quad tree subdivides
}
// Insert a boid in the quad tree
insert(boid) {
// Return if the boid is not in the area of this layer of quad tree
if (!this.boundary.contains(boid)) {
return false;
}
// Add the boid at this layer or a deeper layer depending on capacity
if (this.boids.length < this.capacity) {
// Add the point to this layer if there is still room for it
this.boids.push(boid);
return true;
} else {
// Otherwise, subdivide to make room for the new boid
// Subdivision divides the quad tree area into 8 new children quad trees
if (!this.divided) {
this.subdivide();
}
// Add the boid to the relevant subdivision
// N = North, S = South, E = East, W = West, B = Bottom, T = Top
if (this.NWT.insert(boid)) {
return true;
} else if (this.NET.insert(boid)) {
return true;
} else if (this.SET.insert(boid)) {
return true;
} else if (this.SWT.insert(boid)) {
return true;
} else if (this.NWB.insert(boid)) {
return true;
} else if (this.NEB.insert(boid)) {
return true;
} else if (this.SEB.insert(boid)) {
return true;
} else if (this.SWB.insert(boid)) {
return true;
}
}
}
// Subdivides the quad tree if it is at full capacity, creating 8 new children quad trees
subdivide() {
this.divided = true; // Informs of the subdivision to only subdivide once
let x = this.boundary.x;
let y = this.boundary.y;
let z = this.boundary.z;
let w = this.boundary.w / 2;
let h = this.boundary.h / 2;
let d = this.boundary.d / 2;
// Creates the 8 children quad trees with the relevant positions and area
// North West Top quad tree
let NWTBoundary = new Cube(x - w, y - h, z - d, w, h, d);
this.NWT = new QuadTree(NWTBoundary, this.capacity);
// North East Top quad tree
let NETBoundary = new Cube(x + w, y - h, z - d, w, h, d);
this.NET = new QuadTree(NETBoundary, this.capacity);
// South East Top quad tree
let SETBoundary = new Cube(x + w, y + h, z - d, w, h, d);
this.SET = new QuadTree(SETBoundary, this.capacity);
// South West Top quad tree
let SWTBoundary = new Cube(x - w, y + h, z - d, w, h, d);
this.SWT = new QuadTree(SWTBoundary, this.capacity);
// North West Bot quad tree
let NWBBoundary = new Cube(x - w, y - h, z + d, w, h, d);
this.NWB = new QuadTree(NWBBoundary, this.capacity);
// North East Bot quad tree
let NEBBoundary = new Cube(x + w, y - h, z + d, w, h, d);
this.NEB = new QuadTree(NEBBoundary, this.capacity);
// South East Bot quad tree
let SEBBoundary = new Cube(x + w, y + h, z + d, w, h, d);
this.SEB = new QuadTree(SEBBoundary, this.capacity);
// South West Bot quad tree
let SWBBoundary = new Cube(x - w, y + h, z + d, w, h, d);
this.SWB = new QuadTree(SWBBoundary, this.capacity);
}
// Returns all the points in a given range (Cube) and put them in the "found" array
query(range, found) {
// The array "found" will check all quad trees intersecting with the range,
// looking for points intersecting with the range
if (!found) found = []; // Creates the array at the beginning of the recursion
if (!this.boundary.intersects(range)) {
return found; // No intersection between the quad tree and the range, no need to check for points
} else {
// If the range intersects this quad tree, check for the intersection of its points with the range
for (let boid of this.boids) {
if (range.contains(boid)) {
found.push(boid); // Add the points intersecting with the range to "found"
}
}
// This quad tree intersects with the range, now do the same for its children quad trees
if (this.divided) {
this.NWT.query(range, found);
this.NET.query(range, found);
this.SET.query(range, found);
this.SWT.query(range, found);
this.NWB.query(range, found);
this.NEB.query(range, found);
this.SEB.query(range, found);
this.SWB.query(range, found);
}
}
return found;
}
}
}
================================================
FILE: src/components/PageSwitcher/components/EEGEduAnimate/sketchTone.js
================================================
import p5 from "p5";
import "p5/lib/addons/p5.sound";
export default function sketchTone (p) {
let delta = 0;
let theta = 0;
let alpha = 0;
let beta = 0;
let gamma = 0;
let osc, envelope, fft;
let scaleArray = [delta+30, theta+50 , beta+60, gamma*10+70];
let note = 0;
p.setup = function () {
p.createCanvas(710, 200);
osc = new p5.SinOsc();
// Instantiate the envelope
envelope = new p5.Env();
// set attackTime, decayTime, sustainRatio, releaseTime
envelope.setADSR(0.001, 0.5, 0.1, 0.5);
// set attackLevel, releaseLevel
envelope.setRange(1, 0);
osc.start();
fft = new p5.FFT();
p.noStroke();
};
p.myCustomRedrawAccordingToNewPropsHandler = function (props) {
delta = Math.floor(props.delta);
theta = Math.floor(props.theta);
alpha = Math.floor(props.alpha);
beta = Math.floor(props.beta);
gamma = Math.floor(props.gamma);
};
p.windowResized = function() {
p.resizeCanvas(p.windowWidth*.6, 200);
}
p.draw = function () {
p.background(20);
if (p.frameCount % alpha === 0 || p.frameCount === 1) {
let midiValue = scaleArray[note];
let freqValue = p.midiToFreq(midiValue);
osc.freq(freqValue);
envelope.play(osc, 0, 0.1);
note = (note + 1) % scaleArray.length;
}
let spectrum = fft.analyze();
for (let i = 0; i < spectrum.length / 20; i++) {
p.fill(spectrum[i], spectrum[i] / 10, 0);
let x = p.map(i, 0, spectrum.length / 20, 0, p.width);
let h = p.map(spectrum[i], 0, 255, 0, p.height);
p.rect(x, p.height, spectrum.length / 20, -h);
}
};
};
================================================
FILE: src/components/PageSwitcher/components/EEGEduAnimate/translations/en.json
================================================
{
"title": "P5js integration",
"description": [
"In the next demo we look at the traditional frequency bands. ",
"This time instead of graphing them we pipe them into a live web animation. ",
"For simplicity for now we will only use the left frontal electrode AF7. ",
"All of these animations are embedded p5js animations."
]
}
================================================
FILE: src/components/PageSwitcher/components/EEGEduBands/EEGEduBands.js
================================================
import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal, Link } from "@shopify/polaris";
import { saveAs } from 'file-saver';
import { takeUntil } from "rxjs/operators";
import { Subject, timer } from "rxjs";
import { channelNames } from "muse-js";
import { Bar } from "react-chartjs-2";
import { zipSamples } from "muse-js";
import {
bandpassFilter,
epoch,
fft,
powerByBand
} from "@neurosity/pipes";
import { chartStyles, generalOptions } from "../chartOptions";
import * as generalTranslations from "../translations/en";
import * as specificTranslations from "./translations/en";
import { bandLabels } from "../../utils/chartUtils";
export function getSettings () {
return {
cutOffLow: 2,
cutOffHigh: 50,
interval: 100,
bins: 256,
duration: 1024,
srate: 256,
name: 'Bands',
secondsToSave: 10
}
};
export function buildPipe(Settings) {
if (window.subscriptionBands) window.subscriptionBands.unsubscribe();
window.pipeBands$ = null;
window.multicastBands$ = null;
window.subscriptionBands = null;
// Build Pipe
window.pipeBands$ = zipSamples(window.source.eegReadings$).pipe(
bandpassFilter({
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
nbChannels: window.nchans }),
epoch({
duration: Settings.duration,
interval: Settings.interval,
samplingRate: Settings.srate
}),
fft({ bins: Settings.bins }),
powerByBand(),
catchError(err => {
console.log(err);
})
);
window.multicastBands$ = window.pipeBands$.pipe(
multicast(() => new Subject())
);
}
export function setup(setData, Settings) {
console.log("Subscribing to " + Settings.name);
if (window.multicastBands$) {
window.subscriptionBands = window.multicastBands$.subscribe(data => {
setData(bandsData => {
Object.values(bandsData).forEach((channel, index) => {
channel.datasets[0].data = [
data.delta[index],
data.theta[index],
data.alpha[index],
data.beta[index],
data.gamma[index]
];
channel.xLabels = bandLabels;
});
return {
ch0: bandsData.ch0,
ch1: bandsData.ch1,
ch2: bandsData.ch2,
ch3: bandsData.ch3,
ch4: bandsData.ch4
};
});
});
window.multicastBands$.connect();
console.log("Subscribed to " + Settings.name);
}
}
export function renderModule(channels) {
function renderCharts() {
return Object.values(channels.data).map((channel, index) => {
if (index === 0) {
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: {
min: 0,
max: 100
}
}
]
},
title: {
...generalOptions.title,
text: 'Power by Frequency Band'
}
};
if (channels.data.ch3.datasets[0].data) {
const newData = {
datasets: [{
label: channelNames[0],
backgroundColor: 'rgba(217,95,2)',
data: channels.data.ch0.datasets[0].data,
fill: false
}, {
label: channelNames[1],
backgroundColor: 'rgba(27,158,119)',
data: channels.data.ch1.datasets[0].data,
fill: false
}, {
label: channelNames[2],
backgroundColor: 'rgba(117,112,179)',
data: channels.data.ch2.datasets[0].data,
fill: false
}, {
label: channelNames[3],
backgroundColor: 'rgba(231,41,138)',
data: channels.data.ch3.datasets[0].data,
fill: false
}, {
label: channelNames[4],
backgroundColor: 'rgba(20,20,20)',
data: channels.data.ch4.datasets[0].data,
fill: false
}],
xLabels: channels.data.ch0.xLabels
}
return (
<Card.Section key={"Card_" + 1}>
<Bar key={"Line_" + 1} data={newData} options={options} />
</Card.Section>
);
} else {
return(
<Card.Section>
<TextContainer>
<p> {[
"Press connect above to see the chart."
]}
</p>
</TextContainer>
</Card.Section>
)
}
} else {
return null
}
});
}
return (
<Card title={specificTranslations.title}>
<Card.Section>
<Stack>
<TextContainer>
<p>{[
"In the next demo we look at the traditional frequency bands. ",
"Oscillations in the brain are important as a mechinasm of brain function and communication. ",
"For example, within a brain area, cellular firing becomes locked to the ongoing oscillations of the local field potential: "
]}</p>
</TextContainer>
<img
src={ require("./phaseLockedFiring.png")}
alt="phaseLocked"
width="50%"
height="auto"
></img>
</Stack>
<Stack>
<Link url="hhttps://en.wikipedia.org/wiki/Neural_oscillation#/media/File:SimulationNeuralOscillations.png"
external={true}>
Image Source - Wikipedia </Link>
<br />
<TextContainer>
<p>{[
"Since oscillations can control the timing of neural firing, they can also be used to communciate information and create distributed representations. ",
"Two nearby brain regions that oscillate in sync will have cells that also fire in sync, which also means the neurons they connect to will be more influenced. ",
"Different brain regions have different frequency oscillations at different times "
]}</p>
</TextContainer>
<TextContainer>
<p>{[
"Oscillations in the brain seem to belong to a number of basic families or frequency bands that are influenced by cognition in different ways. ",
"We take the same spectra that was computed in Spectra and divide into five bands. ",
"Delta (1-4 Hz), Theta (4-7 Hz), Alpha (7-12 Hz), Beta (12-30 Hz), and Gamma (30+ Hz). "
]}</p>
</TextContainer>
<img
src={ require("./freqBands.jpg")}
alt="freqBands"
width="50%"
height="auto"
></img>
</Stack>
<Stack>
<Link url="https://upload.wikimedia.org/wikipedia/commons/5/59/Analyse_spectrale_d%27un_EEG.jpg"
external={true}>
Image Source - Wikipedia </Link>
<br />
<TextContainer>
<p>{[
"These different frequency bands are associated with different brain states. ",
"For example, when we pay focus attention to something, our alpha goes down and our beta oscillations increase. ",
"Gamma oscillations are associated with neural activity, but for the most part Gamma oscillations are very difficult to measure outside the head (very small) and so we will ignore them. ",
"Theta oscillations increase during spatial navigation and are associated with learning and memory. ",
"Alpha oscillations are assocaited with attention and active inhbition of neural activity, we will consider them further below. "
]}</p>
</TextContainer>
</Stack>
</Card.Section>
<Card.Section>
<Stack>
<TextContainer>
<p>{[
"Connect a muse and watch the following bar chart of the frequency band power. There is bar for each electrode. ",
"See if you can pick one of the frequency bands and try to control the height by relaxing."
]}</p>
</TextContainer>
<img
src={ require("./electrodediagram.png")}
alt="Electrodes"
width="20%"
height="auto"
></img>
</Stack>
</Card.Section>
<Card.Section>
<div style={chartStyles.wrapperStyle.style}>{renderCharts()}</div>
</Card.Section>
</Card>
);
}
// https://en.wikipedia.org/wiki/Neural_oscillation#/media/File:SimulationNeuralOscillations.png
// https://upload.wikimedia.org/wikipedia/commons/5/59/Analyse_spectrale_d%27un_EEG.jpg
//
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={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}
/>
</Card>
)
}
export function renderRecord(recordPopChange, recordPop, status, Settings, setSettings) {
function handleSecondsToSaveRangeSliderChange(value) {
setSettings(prevState => ({...prevState, secondsToSave: value}));
}
return (
<Card title={'Record Data'} sectioned>
<Stack>
<TextContainer>
<p>{[
"One of the earliest and easiest to measure changes in the EEG is that of alpha oscillations when the eyes closed. ",
"We will test these changes by recording data in two conditions, and comparing the average alpha at all four electrodes between conditions. ",
"We expect to replicate the following relationship: "
]}</p>
</TextContainer>
<img
src={ require("./alphaOpenClosed.png")}
alt="closedOpen"
width="50%"
height="auto"
></img>
</Stack>
<Stack>
<Link url="https://www.semanticscholar.org/paper/Coupling-between-visual-alpha-oscillations-and-mode-Mo-Liu/82593c9b9662d4dc022d51607b313f851f670246"
external={true}>
Image Source - Mo et al., 2013, Neuroimage </Link>
<br />
<br />
<br />
<TextContainer>
<p>{[
"First go to the Raw module 3 and check the data and connection quality. ",
"Then come back to Module 6, no need to change any settings. ",
"We will record two sessions for each person in your group, one with eyes open and one with eyes closed. ",
"Once recorded you can open the .csv file and observe what gets saved. Along the rows are the different frequency bands from each electrode, we are going to average over all four electrodes in this assignment. ",
"We are also going to average over time, which is shown on different rows. ",
"So please compute the average ALPHA power in that output file, and do the same for the other condition, make sure to keep track of which file was created during which condition. ",
"Compare your values for eyes open vs eyes closed, did you find the expected difference? ",
"Why do you think alpha differs when we close our eyes? "
]}</p>
</TextContainer>
<TextContainer>
<p>{[
"Once you are complete, move on to the next Module and control live animations with the values of these frequency bands. ",
]}</p>
</TextContainer>
<RangeSlider
disabled={status === generalTranslations.connect}
min={2}
max={180}
label={'Recording Length: ' + Settings.secondsToSave + ' Seconds'}
value={Settings.secondsToSave}
onChange={handleSecondsToSaveRangeSliderChange}
/>
<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) {
console.log('Saving ' + Settings.secondsToSave + ' seconds...');
var localObservable$ = null;
const dataToSave = [];
console.log('making ' + Settings.name + ' headers')
dataToSave.push(
"Timestamp (ms),",
"delta0,delta1,delta2,delta3,deltaAux,",
"theta0,theta1,theta2,theta3,thetaAux,",
"alpha0,alpha1,alpha2,alpha3,alphaAux,",
"beta0,beta1,beta2,beta3,betaAux,",
"gamma0,gamma1,gamma2,gamma3,gammaAux\n"
);
// Create timer
const timer$ = timer(Settings.secondsToSave * 1000);
// put selected observable object into local and start taking samples
localObservable$ = window.multicastBands$.pipe(
takeUntil(timer$)
);
// 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');
}
});
}
================================================
FILE: src/components/PageSwitcher/components/EEGEduBands/translations/en.json
================================================
{
"title": "Frequency Bands Data",
"xlabel": "Frequency (Hz)",
"ylabel": "Power (\u03BCV\u00B2)"
}
================================================
FILE: src/components/PageSwitcher/components/EEGEduEvoked/EEGEduEvoked.js
================================================
import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { Subject, timer } from "rxjs";
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
import { saveAs } from 'file-saver';
import { take, takeUntil } from "rxjs/operators";
import { zipSamples } from "muse-js";
import {
bandpassFilter,
epoch
} from "@neurosity/pipes";
import { chartStyles } from "../chartOptions";
import * as generalTranslations from "../translations/en";
import * as specificTranslations from "./translations/en";
import { generateXTics, standardDeviation } from "../../utils/chartUtils";
import P5Wrapper from 'react-p5-wrapper';
import sketchEvoked from './sketchEvoked'
export function getSettings () {
return {
cutOffLow: 2,
cutOffHigh: 20,
interval: 1,
srate: 256,
duration: 1,
name: 'Evoked',
secondsToSave: 60
}
};
export function buildPipe(Settings) {
if (window.subscriptionEvoked) window.subscriptionEvoked.unsubscribe();
window.pipeEvoked$ = null;
window.multicastEvoked$ = null;
window.subscriptionEvoked = null;
// Build Pipe
window.pipeEvoked$ = zipSamples(window.source.eegReadings$).pipe(
bandpassFilter({
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
nbChannels: window.nchans }),
epoch({
duration: Settings.duration,
interval: Settings.interval,
samplingRate: Settings.srate
}),
catchError(err => {
console.log(err);
})
);
window.multicastEvoked$ = window.pipeEvoked$.pipe(
multicast(() => new Subject())
);
}
export function setup(setData, Settings) {
console.log("Subscribing to " + Settings.name);
if (window.multicastEvoked$) {
window.subscriptionEvoked = window.multicastEvoked$.subscribe(data => {
setData(evokedData => {
Object.values(evokedData).forEach((channel, index) => {
channel.datasets[0].data = data.data[index];
channel.xLabels = generateXTics(Settings.srate, Settings.duration);
channel.datasets[0].qual = standardDeviation(data.data[index])
});
return {
ch0: evokedData.ch0,
ch1: evokedData.ch1,
ch2: evokedData.ch2,
ch3: evokedData.ch3,
ch4: evokedData.ch4
};
});
});
window.multicastEvoked$.connect();
console.log("Subscribed to Evoked");
}
}
export function renderModule(channels) {
function renderCharts() {
return null
}
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, setSettings) {
function handleSecondsToSaveRangeSliderChange(value) {
setSettings(prevState => ({...prevState, secondsToSave: value}));
}
return (
<Card title={'Run ERP experiment'} sectioned>
<Card.Section>
<p>
{"Clicking this button will begin the experiment so check your data quality on the raw module first. "}
{"A window will pop up when you click the button and a series of circles will appear. Stare at the cross in the center. "}
{"There will be red and blue circles, ignore the blue ones."}
{"Whenever you see a red circle, as fast as you can either press spacebar on a keyboard, or tap the touchscreen on a tablet or phone. "}
{"This entire time the eeg data will be saved along with a column indicating which target was on the screen, and another for the responses. "}
{"The task will continue for a few minutes and once it is finished a .csv file will automatically download. "}
{"This .csv file has a row for each time point, a column for each electrode, and the columns indicating when targets appeared, and when responses were made. "}
{"It saves a marker of 20 when the target is on, a marker of 10 when the blue standards are on, and a peak when the spacebar or touchscreen are pressed, here you can see those synced with an eeg channel: "}
</p>
<p>
<img
src={ require("./dataExample.png")}
alt="dataExample"
width="100%"
height="auto"
></img>
</p>
</Card.Section>
<Stack>
<RangeSlider
disabled={status === generalTranslations.connect}
min={2}
max={180}
label={'Recording Length: ' + Settings.secondsToSave + ' Seconds'}
value={Settings.secondsToSave}
onChange={handleSecondsToSaveRangeSliderChange}
/>
<ButtonGroup>
<Button
onClick={() => {
saveToCSV(Settings);
recordPopChange();
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect}
>
{'Run oddball experiment'}
</Button>
</ButtonGroup>
<Modal
open={recordPop}
onClose={recordPopChange}
title={"Press Spacebar or tap screen when you see RED circle"}
>
<Modal.Section>
<Card.Section>
<P5Wrapper sketch={sketchEvoked} />
</Card.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) {
console.log('Saving ' + Settings.secondsToSave + ' seconds...');
var localObservable$ = null;
const dataToSave = [];
window.marker = 0;
window.responseMarker = 0;
window.touchMarker = 0;
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.multicastEvoked$.pipe(
take(1)
);
//take one sample to get header info
localObservable$.subscribe({
next(x) {
dataToSave.push(
"Timestamp (ms),",
"Marker,",
"SpaceBar,",
"TouchMarker,",
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"
);
}
});
//create timer
const timer$ = timer(Settings.secondsToSave * 1000)
// put selected observable object into local and start taking samples
localObservable$ = window.multicastEvoked$.pipe(
takeUntil(timer$)
);
// now with header in place subscribe to each epoch and log it
localObservable$.subscribe({
next(x) {
dataToSave.push(
Date.now() + "," +
window.marker + "," +
window.responseMarker + "," +
window.touchMarker + "," +
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');
}
});
}
================================================
FILE: src/components/PageSwitcher/components/EEGEduEvoked/sketchEvoked.js
================================================
export default function sketchEvoked (p) {
let x = 0;
let thisRand = 0.5; //for random choice of target type
let targProp = 0.25;
let isTarget = false;
let ellapsedTime = 0;
let nextDelay = 1000;
let newOnset = true;
let startTime = 0;
let targCount = 0;
p.setup = function () {
p.createCanvas(300, 300);
p.frameRate(60);
p.noStroke();
};
p.windowResized = function() {
p.createCanvas(300, 300);
}
p.touchStarted = function() {
if (window.touchMarker === 0) {
window.touchMarker = 255;
} else {
window.touchMarker = 0;
}
}
p.draw = function () {
if (p.keyIsPressed === true) {
window.responseMarker = p.keyCode;
} else {
window.responseMarker = 0;
}
p.background(255);
x = x+1;
ellapsedTime = p.millis()-startTime;
if (ellapsedTime > nextDelay) {
newOnset = true;
} else {
newOnset = false;
}
if (newOnset) {
targCount++;
nextDelay = 500 + p.int(p.random() * 1000);
console.log(targCount, nextDelay)
startTime = p.millis();
thisRand = p.random();
if (thisRand < targProp) { // targets 20% of the time
isTarget = true;
} else {
isTarget = false;
}
if (isTarget) {
p.fill(250, 150, 150);
window.marker = 20;
} else {
p.fill(150,150,250);
window.marker = 10;
}
} else { // during time between targets
p.fill(255, 255, 255);
window.marker = 0;
}
p.ellipse(p.width/2, p.height/2, 300);
p.fill(255,0,0);
p.text("+", p.width/2, p.height/2);
}
};
================================================
FILE: src/components/PageSwitcher/components/EEGEduEvoked/translations/en.json
================================================
{
"title": "Stimulus Evoked Event-related potential (ERP)",
"description": [
"The electrical activity evoked by individual presentations of stimuli does create small changes in electrical voltage. ",
"These changes are, however, very small, about 1-10 microVolts (\u03BCV), and the noise from other things like eye movements and muscles is sometimes 10x as large. ",
"This noise can be mitigated by averaging this evoked activity over repeated presentations of the same stimulus. ",
"This average voltage in response to a stimulus is called an Event-related potential (ERP) or evoked potential (EP). ",
"Here..."
],
"xlabel": "Time (ms)",
"ylabel": "Votage (\u03BCV)"
}
================================================
FILE: src/components/PageSwitcher/components/EEGEduHeartRaw/EEGEduHeartRaw.js
================================================
import React from "react";
import { catchError, multicast, take } from "rxjs/operators";
import { Subject } from "rxjs";
import { TextContainer, Card, Stack, ButtonGroup, Button, Modal, Link } from "@shopify/polaris";
import { channelNames } from "muse-js";
import { Line } from "react-chartjs-2";
import { saveAs } from 'file-saver';
import YouTube from 'react-youtube'
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,
interval: 10,
srate: 256,
duration: 2560,
name: 'HeartRaw'
}
};
export function buildPipe(Settings) {
if (window.subscriptionHeartRaw) window.subscriptionHeartRaw.unsubscribe();
window.pipeHeartRaw$ = null;
window.multicastHeartRaw$ = null;
window.subscriptionHeartRaw = null;
// Build Pipe
window.pipeHeartRaw$ = zipSamples(window.source.eegReadings$).pipe(
bandpassFilter({
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
nbChannels: window.nchans }),
epoch({
duration: Settings.duration,
interval: Settings.interval,
samplingRate: Settings.srate
}),
catchError(err => {
console.log(err);
})
);
window.multicastHeartRaw$ = window.pipeHeartRaw$.pipe(
multicast(() => new Subject())
);
}
export function setup(setData, Settings) {
console.log("Subscribing to " + Settings.name);
if (window.multicastHeartRaw$) {
window.subscriptionHeartRaw = window.multicastHeartRaw$.subscribe(data => {
setData(heartRawData => {
Object.values(heartRawData).forEach((channel, index) => {
channel.datasets[0].data = data.data[index];
channel.xLabels = generateXTics(Settings.srate, Settings.duration).map(function(x) {return x / 1000});;
channel.datasets[0].qual = standardDeviation(data.data[index])
});
return {
ch0: heartRawData.ch0,
ch1: heartRawData.ch1
};
});
});
window.multicastHeartRaw$.connect();
console.log("Subscribed to HeartRaw");
}
}
export function renderModule(channels) {
function renderCharts() {
return Object.values(channels.data).map((channel, index) => {
if (index === 1) {
const options = {
...generalOptions,
scales: {
xAxes: [{
scaleLabel: {
...generalOptions.scales.xAxes[0].scaleLabel,
labelString: specificTranslations.xlabel
},
gridLines: {
color: "rgba(50,50,50)"
},
ticks: {
maxTicksLimit: 10
}
}],
yAxes: [
{
scaleLabel: {
...generalOptions.scales.yAxes[0].scaleLabel,
labelString: specificTranslations.ylabel
}
}
]
},
elements: {
line: {
borderColor: 'rgba(' + channel.datasets[0].qual + ', 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>
);
} else {
return null
}
});
}
return (
<React.Fragment>
<Card title={specificTranslations.title}>
<Card.Section>
<Stack>
<TextContainer>
<p> {[
"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). You have seen this hundreds of times in movies and TV beside hospital beds. "
]} </p>
</TextContainer>
<br />
<br />
<img
src={ require("./ECG_Principle_fast.gif")}
alt="ECGPrinciples"
width="50%"
height="auto"
></img>
<Link url="https://commons.wikimedia.org/wiki/File:ECG_Principle_fast.gif"> Image Source - Wikipedia </Link>
<br />
<TextContainer>
<p> {[
"The ECG is best measured by comparing the electrical potential accross the left vs. right side of the body. ",
"Depending on where on the body the measurements are taken, they pick up a different view of the electrical potentials generated by the heart. ",
"One of the easiest places to measure is comparing the voltage between the left and right hands. ",
"For example, this is how treadmills can read your heart rate when you place one hand on each of the two holds. "
]} </p>
</TextContainer>
<br />
<img
src={ require("./LimbLead.png")}
alt="LeftHand"
width="50%"
height="auto"
></img>
<Link url="https://en.wikipedia.org/wiki/Electrocardiography#/media/File:Limb_leads_of_EKG.png"> Image Source - Wikipedia </Link>
<br />
<TextContainer>
<p> {[
"Therefore, you can see your own ECG today using the muse. ",
"You can take off the muse from your head and place a finger on your right hand on the muse's reference electrode (in the center of the forehead). ",
"We can then place a finger of our left hand on one of the eeg electrodes. Lets use the left forehead electrode (position AF7). ",
"So place your left fingers pinching the left forehead electrode, and your right fingers pinching the center electrode. ",
"Otherwise try to hold the Muse as still as possible, and relax your body. "
]} </p>
</TextContainer>
<br />
</Stack>
<img
src={ require("./LeftHand.png")}
alt="LeftHand"
width="25%"
height="auto"
></img>
<img
src={ require("./RightHand.png")}
alt="RightHand"
width="25%"
height="auto"
></img>
<br />
<Stack>
<br />
<img
src={ require("./electrodediagram2.png")}
alt="F7Electrode"
width="25%"
height="auto"
></img>
<Link url="https://github.com/NeuroTechX/eeg-101/blob/master/EEG101/src/assets/electrodediagram2.png"> Image Source - EEG101 </Link>
<TextContainer>
<p> {[
"If you haven't already, connect your muse and try to plot your heart rate before moving on to the data recording and analysis below. ",
"Soon you should see spikes in voltage measured between those two electrodes each time your heart beats. ",
"Watch what happens when you move your body or fingers, and notice how delicate the signal is. "
]} </p>
</TextContainer>
</Stack>
<br />
</Card.Section>
</Card>
<Card title={"Live Data"}>
<Card.Section>
<TextContainer>
<p> {[
"Here time is on the horizontal axis, and the voltage is on the vertical axis. ",
"There are 10 Seconds of data shown, with the current time shown on the right. ",
"There is a vertical line every second so you can estimate your heart rate roughly. ",
"Below we will save data and use it to estimate your heart more accurately in a spreadsheet. ",
"the signal will be red if it is noisy, and when you relax and hold still it will turn green/black"
]} </p>
</TextContainer>
<div style={chartStyles.wrapperStyle.style}>{renderCharts()}</div>
</Card.Section>
</Card>
</React.Fragment>
);
}
export function renderRecord(recordPopChange, recordPop, status, Settings) {
const opts = {
height: '195',
width: '320',
playerVars: { // https://developers.google.com/youtube/player_parameters
autoplay: false
}
};
return (
<Card title={'Collect Raw Heart Rate Data'} sectioned>
<Card.Section>
<Stack>
<TextContainer>
<p> {[
"Clicking this button will immediately record a 10 second long segment of data just like it is shown in the plot above. ",
"Therefore, make sure the chart above looks clean and you can see you heart beat clearly before pressing record. ",
"We are going to compare two conditions that show a clear difference in heart rate due to blood pressure changes: ",
"Standing and Sitting. ",
"You will record two sessions, one standing and one sitting, pick the order randomly but keep track of which output file is which"
]} </p>
</TextContainer>
<ButtonGroup>
<Button
onClick={() => {
recordPopChange()
saveToCSV(Settings);
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect}
>
{'Record Raw ECG Data'}
</Button>
</ButtonGroup>
<TextContainer>
<p> {[
"A .csv file will be saved that can be opened in your favorite spreadsheet software like Microsoft Excel or in our examples, Google Sheets. ",
"Remember for each person at your computer to record two files, one while they are standing and one while they are sitting. ",
"Here is an example of what the data will look like once loaded. ",
"The top row shows the millsecond ellapsed in the data segment from 4 to 10,000. ",
"The bottom row shows the voltage measured from the difference between the two electrode, for each of those moments in time. ",
"The plot is plotting the data for each time point, this time with zero on the left. "
]} </p>
</TextContainer>
<img
src={ require("./PlotEKG_Sheets.png")}
alt="F7Electrode"
width="75%"
height="auto"
></img>
<TextContainer>
<p> {[
"The following youtube video will show you how to open the file in Google Sheets, rename it, plot the data, find the peaks, ",
"Record them and find their difference in time, then take the average difference to estimate your average heart period. ",
"This value is then used to estimate your heart rate in beats per minute. "
]}
<Link url="https://docs.google.com/spreadsheets/d/1v2JfPkkiSiXizY9SZOhsliSpCyWDv6oW0ESu-wAL-DY/edit?usp=sharing">
Link to example google sheet from video.
</Link>
</p>
</TextContainer>
<br />
<br />
<YouTube
videoId="GyIofXQUOvE"
opts={opts}
/>
<br />
<TextContainer>
<p> {[
"Finally each of you will enter this number for both sitting and standing into a google sheet that we are sharing as a class. ",
"We will use this shared google sheet which combines all our data in order to compute group statistics. "
]} </p>
</TextContainer>
<br />
</Stack>
</Card.Section>
<Modal
open={recordPop}
onClose={recordPopChange}
title={"Data is recording"}
>
<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>
</Card>
);
}
function saveToCSV(Settings) {
console.log('Saving ' + Settings.secondsToSave + ' seconds...');
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.multicastHeartRaw$.pipe(
take(1)
);
//take one sample to get header info
localObservable$.subscribe({
next(x) {
dataToSave.push(
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(t) {return t }) + ",",
"\n"
);
}
});
// put selected observable object into local and start taking samples
localObservable$ = window.multicastHeartRaw$.pipe(
take(1)
);
// now with header in place subscribe to each epoch and log it
localObservable$.subscribe({
next(x) {
console.log(x)
dataToSave.push(Object.values(x.data[1]).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');
}
});
}
================================================
FILE: src/components/PageSwitcher/components/EEGEduHeartRaw/translations/en.json
================================================
{
"title": "Electrocardiogram (Heart Beats)",
"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 (Seconds)",
"ylabel": "Voltage (\u03BCV)"
}
================================================
FILE: src/components/PageSwitcher/components/EEGEduHeartSpectra/EEGEduHeartSpectra.js
================================================
import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Link, Modal } from "@shopify/polaris";
import { saveAs } from 'file-saver';
import { takeUntil } from "rxjs/operators";
import { Subject, timer } from "rxjs";
import { channelNames } from "muse-js";
import { Line } from "react-chartjs-2";
import YouTube from 'react-youtube'
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: .01,
cutOffHigh: 20,
interval: 100,
bins: 8192,
sliceFFTLow: 0.5,
sliceFFTHigh: 2.5,
duration: 2048,
srate: 256,
name: 'HeartSpectra',
secondsToSave: 10
}
};
export function buildPipe(Settings) {
if (window.subscriptionHeartHeartSpectra) window.subscriptionHeartSpectra.unsubscribe();
window.pipeHeartSpectra$ = null;
window.multicastHeartSpectra$ = null;
window.subscriptionHeartSpectra = null;
// Build Pipe
window.pipeHeartSpectra$ = zipSamples(window.source.eegReadings$).pipe(
bandpassFilter({
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
nbChannels: window.nchans }),
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.multicastHeartSpectra$ = window.pipeHeartSpectra$.pipe(
multicast(() => new Subject())
);
}
export function setup(setData, Settings) {
console.log("Subscribing to " + Settings.name);
if (window.multicastHeartSpectra$) {
window.subscriptionHeartSpectra = window.multicastHeartSpectra$.subscribe(data => {
setData(heartSpectraData => {
Object.values(heartSpectraData).forEach((channel, index) => {
channel.datasets[0].data = data.psd[1];
channel.xLabels = data.freqs.map(function(x) {return x * 60});
});
return {
ch1: heartSpectraData.ch1,
};
});
});
window.multicastHeartSpectra$.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: {
min: 0
}
}
]
},
elements: {
point: {
radius: 8
}
},
animation: {
duration: 200
},
title: {
...generalOptions.title,
text: generalTranslations.channel +
channelNames[1] +
" - Estimated HR: " +
channel.peakF + " BPM"
}
};
if (index === 0) {
if (channel.xLabels) {
channel.peakInd = indexOfMax(channel.datasets[0].data);
channel.peakF = channel.xLabels[channel.peakInd];
channel.peakVal = channel.datasets[0].data[channel.peakInd]
const newData = {
datasets: [{
label: 'Peak',
borderColor: 'rgba(0,0,0)',
backgroundColor: 'rgba(231,41,138)',
data: [{
x: channel.peakF,
y: channel.peakVal
}],
fill: false
}, {
label: channelNames[0],
borderColor: 'rgba(180,180,180)',
data: channel.datasets[0].data,
fill: true
} ],
xLabels: channel.xLabels
}
return (
<Card.Section key={"Card_" + index}>
<Line key={"Line_" + index} data={newData} options={options} />
</Card.Section>
);
} else {
return(
<Card.Section>
<TextContainer>
<p> {[
"Press connect above to see the chart."
]}
</p>
</TextContainer>
</Card.Section>
)
}
} else {
return null
}
});
}
const opts = {
height: '195',
width: '320',
playerVars: { // https://developers.google.com/youtube/player_parameters
autoplay: false
}
};
return (
<React.Fragment>
<Card title={specificTranslations.title}>
<Card.Section>
<Stack>
<TextContainer>
<p> {[
"In the previous module we each estimated our heart rate in two conditions, while we were sitting, and while we were standing. ",
"We used a shared google sheet which combines all our data in order to compute group statistics. ",
"The following video shows how to use the data to make plots of the data, compute statistics, and test the difference. "
]}
<Link url="https://docs.google.com/spreadsheets/d/1_R4ViDw5VQv72F-lzi9BJyRPx1-BhQsj7XFckrvSW-Y/edit?usp=sharing"
external={true}>
A copy of the anonymized data that you can use to follow along with the video can be found here.
</Link>
</p>
</TextContainer>
<br />
<br />
<YouTube
videoId="uyrnVdKteoU"
opts={opts}
/>
<br />
<TextContainer>
<p> {[
"There are a few problems with the way we estimated heart rate in the previous module. ",
"We used only a single 10 second segment of data which adds variability. ",
"We also used peak detection to find each heart beat, which takes alot of time and is difficult with noisy data. ",
"This required us to make arbirary thesholds to find peaks, and led to some poor estimates in heart rate, as indicated by the number of outliers we needed to remove (~15). ",
"Therefore in this module we will use some signal processing to get a better estimate of heart rate. "
]} </p>
</TextContainer>
</Stack>
</Card.Section>
<Card.Section>
<Stack>
<TextContainer>
<p> {[
"Here is an example of some ECG data from our experiment in Module 2. ",
"Notice that just like many other aspects of our bodies function, our heart rate is a rhythm, that is, it repeats in time at regular intervals. "
]} </p>
</TextContainer>
<img
src={ require("./exampleECG.png")}
alt="ECG"
width="80%"
height="auto"
></img>
<TextContainer>
<p> {[
"Therefore, we can use mathematical techniques such as a fourier transform to estimate what frequency is present in the ECG data. ",
"A fourier transform turns any series of numbers into a summed set of sine waves of different sizes. ",
"The following animation shows how a single time-series of data, can be thought of as the sum of different frequencies of sine waves, each of a different magnitude. ",
"The blue line chart in the animation shows what is called the spectra, and indicates the power at each frequency."
]} </p>
</TextContainer>
<br />
<img
src={ require("./fft_animation.gif")}
alt="FFT"
width="100%"
height="auto"
></img>
</Stack>
<Link url="https://en.wikipedia.org/wiki/Electrocardiography#/media/File:Limb_leads_of_EKG.png"
external={true}>
Image Source - Wikipedia </Link>
<Stack>
<br />
<TextContainer>
<p> {[
"Now we can use the muse to estimate our heart rates, but in different ways. 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. ",
"It may help to return to Module 2 and look at the raw data first. "
]} </p>
</TextContainer>
<TextContainer>
<p> {[
"If you missed Module 2, here are the instructions: You can take off the muse from your head and place a finger on your right hand on the muse's reference electrode (in the center of the forehead). ",
"We can then place a finger of our left hand on one of the eeg electrodes. Lets use the left forehead electrode (position AF7). ",
"So place your left fingers pinching the left forehead electrode, and your right fingers pinching the center electrode. ",
"Otherwise try to hold the Muse as still as possible, and relax your body. "
]} </p>
</TextContainer>
<br />
</Stack>
<img
src={ require("./LeftHand.png")}
alt="LeftHand"
width="25%"
height="auto"
></img>
<img
src={ require("./RightHand.png")}
alt="RightHand"
width="25%"
height="auto"
></img>
<br />
<Stack>
<br />
<img
src={ require("./electrodediagram2.png")}
alt="F7Electrode"
width="25%"
height="auto"
></img>
<Link url="https://github.com/NeuroTechX/eeg-101/blob/master/EEG101/src/assets/electrodediagram2.png"
external={true}>
Image Source - EEG101 </Link>
<TextContainer>
<p> {[
"In this new plot, 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. ",
"The pink ball shows the estimated peak of the spectra, or your estimated heart rate. ",
"In the following experiment, it is only this pink value that will be saved over time while we record. ",
"Here is an example of what the plot should look like with a strong heart rate signal. ",
"Notice that there are two peaks, 60 BPM and 120 BPM. The larger peak is called the 1st Harmonic, a multiple of the true heart rate. ",
"These harmonics are common in the frequency domain: "
]} </p>
</TextContainer>
<img
src={ require("./exampleSpectra.png")}
alt="exampleSpectra"
width="50%"
height="auto"
></img>
</Stack>
</Card.Section>
</Card>
<Card title="Live spectra plot">
<Card.Section>
<div style={chartStyles.wrapperStyle.style}>{renderCharts()}</div>
</Card.Section>
</Card>
</React.Fragment>
);
}
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, setSettings) {
function handleSecondsToSaveRangeSliderChange(value) {
setSettings(prevState => ({...prevState, secondsToSave: value}));
}
const opts = {
height: '195',
width: '320',
playerVars: { // https://developers.google.com/youtube/player_parameters
autoplay: false
}
};
return(
<Card title={'Record ' + Settings.name +' Data'} sectioned>
<Stack>
<TextContainer>
<p> {[
"Clicking this button will immediately record the value of the pink dot above for ",
Settings.secondsToSave,
" seconds. ",
"Therefore, make sure the chart above looks clean and you can see a clear peak in the spectra before pressing record. ",
"We are going to compare two conditions that show a clear difference in heart rate due to blood pressure changes: ",
"Standing and Sitting. ",
"You will record two sessions, one standing and one sitting, pick the order randomly but keep track of which output file is which"
]} </p>
</TextContainer>
<RangeSlider
disabled={status === generalTranslations.connect}
min={2}
max={180}
label={'Recording Length: ' + Settings.secondsToSave + ' Seconds'}
value={Settings.secondsToSave}
onChange={handleSecondsToSaveRangeSliderChange}
/>
<ButtonGroup>
<Button
onClick={() => {
saveToCSV(Settings);
recordPopChange();
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect}
>
{'Save to CSV'}
</Button>
</ButtonGroup>
<TextContainer>
<p> {[
"A .csv file will be saved that can be opened in Google Sheets. ",
"Remember for each person to record two files, one while standing and one while sitting. ",
"Here is an example of what the data will look like once loaded. ",
"The first column shows the time in msec of each estimate of heart rate. The second column shows the heart rate estimate in BPM. ",
"Each row represents an estimate from the previous 8 seconds of data. ",
"This is because you need a long segment of time to estimate frequency of rhythms over time. ",
"Each subsequent row is from an 8 second chunk of data, but is taken about 400 ms after the previous row. ",
"You can see the time values increase about 10000 ms during the recording, representing the 10 seconds of data. ",
"So 10000 milliseconds divided into ~400 ms shifts per row gives us the rough number of rows (~25). ",
"The graph shows the values plotted over time, showing that the 1st Harmonic value of 120 BPM was incorrectly recorded on a few windows. "
]} </p>
</TextContainer>
<img
src={ require("./exampleOutput.png")}
alt="exampleOutput"
width="75%"
height="auto"
></img>
<TextContainer>
<p> {[
"In this module the analysis will be much easier, since much of the work has been done by the webpage with the fourier transform and peak detection. ",
"The following youtube video will show you how to open the file in Google Sheets, rename it, plot the data, remove any harmonics or outliers, ",
"then take the average heart rate over the window as an estimate of your heart rate. "
]}
<Link url="https://docs.google.com/spreadsheets/d/1AcFxuIZDMNfOifgql2vrFy2nSS3g7c14fOxAdptOgQY/edit?usp=sharing"
external={true}>
Link to example google sheet from video.
</Link>
</p>
</TextContainer>
<br />
<YouTube
videoId="6EtzwGXjB9Q"
opts={opts}
/>
<TextContainer>
<p> {[
"Finally each of you will enter this estimated heart rate for both sitting and standing into a new anonymized google sheet that we are sharing as a class. ",
"We will use this shared google sheet which combines all our data in order to compute group statistics and to compare the two methods of estimating our heart rate. "
]} </p>
<ol>
<li>Open up the Module 2&3 Anonymized Data Log and make a copy to work on:
<Link url="https://docs.google.com/spreadsheets/d/1Z77FCC4XhsKR85PuRD7drzD3y4ZCG1xczfrAuTJTD3k/edit?usp=sharing"
external={true}>
Link to spreadsheet
</Link>
</li>
<li>This time your report will focus on comparing the results from module 2 to module 3. Start by explaining that in your report, and explain what the difference was between the two modules measurement.</li>
<li>I have already removed outliers from Module 2, Why are there no outliers in Module 3? Include this in your report.</li>
<li>Recompute the statistics you did for the Module 2 data comparing sitting vs standing, and do the same for module 3. Compute the average, count, standard deviation, and standard error for each column. In our summary Report the mean and standard deviation in each of the four conditions. </li>
<li>Notice how some participants only have a score in one module, let us ignore that for now.</li>
<li>Now for both module 2 and module 3, compute a paired samples t-test comparing sitting vs standing. (You have already done this for module 2). Report the two result in apa format (eg. - <i>t</i>(df) = 3.904; p = .003). </li>
<li>Include bar graphs showing the sitting and standing HR in each of the four conditions. </li>
<li>For both module 2 and module 3, make a new column and compute the difference in heart rate Sitting minus standing (Sitting - Standing). Report the average difference in each condition, as well as the standard deviation of the difference. Show a bar graph of the differences and their standard error.</li>
<li>In which module is the standard deviation larger? What does this mean</li><li>It seems we found the same effect in module 2 and module 3. Both t-tests should be significant, but we also want to test how the scores on each module are related. </li>
<li>The following two steps require individuals to have a score for all four conditions, so first REMOVE ROWS where individuals only had values from one of the modules. This should leave 65 individuals (remember the row names are on the first row). </li>
<li>First we want to test if there is a difference between the differences. This would occur if our methods in module 2 and our methods in module 3 gave different changes in heart rate. Compute a t-test comparing the two difference columns. Report the results in APA format. Interpret the results.</li>
<li>Compute the correlation between the difference measured in module 2 and the difference measured in module 3, show a scatter plot comparing them as well. Interpret the strength of the relationship between the change we measured in module 2 and the change we measured in module 3.</li>
<li>Make a final concluding statement about which methods is better for estimating heart rate and why.</li>
</ol>
</TextContainer>
<br />
<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) {
console.log('Saving ' + Settings.secondsToSave + ' seconds...');
var localObservable$ = null;
const dataToSave = [];
console.log('making ' + Settings.name + ' headers')
dataToSave.push(
"Timestamp (ms),",
"Estimated BPM",
"\n"
);
// Create timer
const timer$ = timer(Settings.secondsToSave * 1000);
// put selected observable object into local and start taking samples
localObservable$ = window.multicastHeartSpectra$.pipe(
takeUntil(timer$)
);
// now with header in place subscribe to each epoch and log it
localObservable$.subscribe({
next(x) {
dataToSave.push(Date.now() + "," + x.freqs[indexOfMax(x.psd[1])]*60 + "\n");
// logging is useful for debugging -yup
console.log();
},
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');
}
});
}
// Find the index of the max value in an array
function indexOfMax(arr) {
if (arr.length === 0) {
return -1;
}
var max = arr[0];
var maxIndex = 0;
for (var i = 1; i < arr.length; i++) {
if (arr[i] > max) {
maxIndex = i;
max = arr[i];
}
}
return maxIndex;
}
================================================
FILE: src/components/PageSwitcher/components/EEGEduHeartSpectra/translations/en.json
================================================
{
"title": "Heart Rate (Beats per minute)",
"xlabel": "Heart Frequency (BPM)",
"ylabel": "Power (\u03BCV\u00B2)"
}
================================================
FILE: src/components/PageSwitcher/components/EEGEduIntro/EEGEduIntro.js
================================================
import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { Subject } from "rxjs";
import { Card, Link } from "@shopify/polaris";
import { Line } from "react-chartjs-2";
import { zipSamples } from "muse-js";
import {
bandpassFilter,
epoch
} from "@neurosity/pipes";
import { chartStyles, generalOptions } from "../chartOptions";
import * as specificTranslations from "./translations/en";
import { generateXTics, standardDeviation } from "../../utils/chartUtils";
export function getSettings () {
return {
name: "Intro",
cutOffLow: 2,
cutOffHigh: 20,
interval: 2,
srate: 256,
duration: 512
}
};
export function buildPipe(Settings) {
if (window.subscriptionIntro$) window.subscriptionIntro$.unsubscribe();
window.pipeIntro$ = null;
window.multicastIntro$ = null;
window.subscriptionIntro = null;
// Build Pipe
window.pipeIntro$ = zipSamples(window.source.eegReadings$).pipe(
bandpassFilter({
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
nbChannels: window.nchans }),
epoch({
duration: Settings.duration,
interval: Settings.interval,
samplingRate: Settings.srate
}),
catchError(err => {
console.log(err);
})
);
window.multicastIntro$ = window.pipeIntro$.pipe(
multicast(() => new Subject())
);
}
export function setup(setData, Settings) {
console.log("Subscribing to " + Settings.name);
if (window.multicastIntro$) {
window.subscriptionIntro = window.multicastIntro$.subscribe(data => {
setData(introData => {
Object.values(introData).forEach((channel, index) => {
if (index === 0) {
channel.datasets[0].data = data.data[index];
channel.xLabels = generateXTics(Settings.srate, Settings.duration);
channel.datasets[0].qual = standardDeviation(data.data[index])
}
});
return {
ch0: introData.ch0
};
});
});
window.multicastIntro$.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: 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: 'Voltage signal over time'
}
};
if (index === 0) {
return (
<Card.Section key={"Card_" + index}>
<Line key={"Line_" + index} data={channel} options={options} />
</Card.Section>
);
} else {
return null
};
});
}
return (
<React.Fragment>
<Card title={specificTranslations.title}>
<Card.Section>
<p>
{specificTranslations.intro1}
</p>
<div style={chartStyles.wrapperStyle.style}>
{renderCharts()}
</div>
<p>
{specificTranslations.intro2}
</p>
</Card.Section>
</Card>
<Card title={specificTranslations.neuronsHead}>
<Card.Section>
<p>
{specificTranslations.neurons1}
</p>
<img
src={ require("./assets/neuronarrow.png")}
alt="Single Neuron"
width="100%"
height="auto"
></img>
<Link url="https://github.com/NeuroTechX/eeg-101/blob/master/EEG101/src/assets/neuronarrow.png"> Image Source - EEG101 </Link>
<br />
<br />
<p>
{specificTranslations.neurons2}
</p>
<img
src={ require("./assets/neuronmultiarrow.png")}
alt="Multiple Neurons"
width="100%"
height="auto"
></img>
<Link url="https://github.com/NeuroTechX/eeg-101/blob/master/EEG101/src/assets/neuronmultiarrow.png"> Image Source - EEG101 </Link>
<br />
<br />
<p>
{specificTranslations.neurons3}
</p>
</Card.Section>
</Card>
<Card title={specificTranslations.oscillationsHead}>
<Card.Section>
<p>
{specificTranslations.oscillations1}
</p>
<img
src={ require("./assets/awakeasleep.gif")}
alt="Awake/Asleep"
width="100%"
height="auto"
></img>
<Link url="https://github.com/NeuroTechX/eeg-101/blob/master/EEG101/src/assets/awakeasleep.gif"> Image Source - EEG101 </Link>
<br />
<br />
<p>
{specificTranslations.oscillations2}
</p>
</Card.Section>
</Card>
<Card title={specificTranslations.hardwareHead}>
<Card.Section>
<p>
{specificTranslations.hardware1}
<br />
<br />
{specificTranslations.hardware2}
</p>
<br />
<img
src={ require("./assets/electrodelocations.png")}
alt="electrode locations"
width="50%"
height="auto"
></img>
<br />
<br />
<Link url="https://github.com/NeuroTechX/eeg-101/blob/master/EEG101/src/assets/electrodelocations.png"> Image Source - EEG101 </Link>
<br />
<br />
<p>
{specificTranslations.hardware3}
<br />
<br />
{specificTranslations.hardware4}
<br />
<img
src={ require("./assets/DigitalDAQv2.png")} //https://upload.wikimedia.org/wikipedia/commons/9/97/DigitalDAQv2.pdf
alt="DAQ diagram"
width="100%"
height="auto"
></img>
<Link url="https://upload.wikimedia.org/wikipedia/commons/9/97/DigitalDAQv2.pdf"> Image Source - Wikipedia </Link>
<br />
<br />
{specificTranslations.hardware5}
</p>
</Card.Section>
</Card>
<Card title={specificTranslations.museHead}>
<Card.Section>
<p>
{specificTranslations.muse1}
<br />
<br />
<img
src={ require("./assets/musepicture.png")}
alt="Awake/Asleep"
width="75%"
height="auto"
></img>
<br />
<br />
<Link url="https://miro.medium.com/max/2854/1*pK_tLFd8c7_xlOTm1lHdAw.png"> Image Source - @urish </Link>
<br />
<br />
{specificTranslations.muse2}
<br />
<img
src={ require("./assets/electrodediagram.png")}
alt="Muse Electrodes"
width="50%"
height="auto"
></img>
<br />
<img
src={ require("./assets/electrodelegend.png")}
alt="ElectrodeLegend"
width="50%"
height="auto"
></img>
<br />
<br />
<Link url="https://github.com/NeuroTechX/eeg-101/blob/master/EEG101/src/assets/electrodediagram"> Image Source - EEG101 </Link>
<br />
<br />
{specificTranslations.muse3}
</p>
</Card.Section>
</Card>
<Card title={specificTranslations.signalHead}>
<Card.Section>
<p>
{specificTranslations.signal1}
<br />
<img
src={ require("./assets/electrodediagram1.png")}
alt="SingleElectrode"
width="50%"
height="auto"
></img>
<br />
<Link url="https://github.com/NeuroTechX/eeg-101/blob/master/EEG101/src/assets/electrodediagram"> Image Source - EEG101 </Link>
<br />
<br />
</p>
<div style={chartStyles.wrapperStyle.style}>
{renderCharts()}
</div>
<p>
{specificTranslations.signal2}
</p>
</Card.Section>
</Card>
<Card title={specificTranslations.creditsHead}>
<Card.Section>
<p>
{specificTranslations.credits1}
<Link url="http://learn.neurotechedu.com/">NeurotechEdu. </Link>
</p>
<p>
{specificTranslations.credits2}
<Link url="https://choosemuse.com/muse-research/">Interaxon. </Link>
</p>
<p>
{specificTranslations.credits3}
<Link url="https://github.com/urish/muse-js">muse-js </Link>
{specificTranslations.credits4}
<Link url="https://medium.com/neurotechx/a-techys-introduction-to-neuroscience-3f492df4d3bf">A Techy's Introduction to Neuroscience. </Link>
</p>
<p>
{specificTranslations.credits5}
<Link url="https://github.com/neurosity/eeg-pipes">eeg-pipes </Link>
{specificTranslations.credits6}
<Link url="https://medium.com/@castillo.io/muse-2016-headband-web-bluetooth-11ddcfa74c83">Muse 2016 Headband + Web Bluetooth.</Link>
</p>
</Card.Section>
</Card>
</React.Fragment>
);
}
================================================
FILE: src/components/PageSwitcher/components/EEGEduIntro/translations/en.json
================================================
{
"title": "Introduction",
"intro1": [
"Below you are now looking at an live measurement of the electrical potential created by your brain. ",
"Just like a AA battery stores 1.5 volts of electrical potential energy accross its positive and negative leads, ",
"two points on your head also have a much smaller electrical potential when measured accross them. ",
"Here we are watching that live, with the voltage on the vertical axis (\u03BCV are microvolts, 1 million microvolts are in a volt). ",
"Time is shown in milleseconds along the horizontal axis, with the right side of the chart being the current moment:"
],
"intro2": [
"Read on to find out where this electrical potential comes from and what it means. "
],
"neuronsHead": "Neurons",
"neurons1": [
"The brain is made up of cells called neurons. ",
"Neurons communicate using chemical messages that change the electrical potential of the cells they connect with. ",
"This change in electrical potential, if large enough, can make those cells send messages as well, and so on. ",
"For example, an excitatory neuron releases Glutamate on another neuron, which lets in positively charged Sodium ions into the cell and make its interior less negative compared to outside. ",
"These changes in electrical potential create small electrical fields, which act as tiny electrical dipoles like batteries. "
],
"neurons2": [
"The electrical potential accross the cell membrane is small, around -70 mV at rest (1000 microvolts in a millivolt), and it changes around -20 mV during electrical changes in the cell. ",
"However, if a large group of these tiny dipoles are aligned in space and their electrical potentials change at the same time, ",
"they can create electrical potentials which are large enough to conduct through the brain tissue and be measurable comparing different points on the head. "
],
"neurons3": [
"As you can see above, these electrical potentials measured on the outside of the head fluctuature between about -200 and 200 \u03BCV. ",
"You can also see cycles between high and low voltage, called oscillations, which can occur in the human brain at a number of frequencies. ",
"You may have heard of some of these brain waves before, but what do they mean? "
],
"oscillationsHead": "Oscillations",
"oscillations1": [
"Large groups of aligned neurons are all becoming more and less active together in groups. ",
"And these fluctuations in activity seem to occur within certain frequency bands. ",
"It has been proposed that these different frequencies of neural activity serve functional mechanisms in the brain. ",
"That is, one of the ways the brain uses to process and communicate information is through these rhythmic processes. ",
"These oscillations can change during different behaviours. One of the most drastic is the difference in the EEG when we fall asleep: "
],
"oscillations2": [
"When we are awake, our EEG signal is dominated by high frequency activity called Beta waves. ",
"When we fall asleep, our brain slows down. Larger and larger groups of neurons all fire together, in slow oscillations called Delta waves. ",
"We will learn more in the modules on the frequency spectra and frequency bands about what the various oscillations represent and how they change. ",
"We use the power of these brain waves to provide real time feedback about the state of your brain and practice Neurofeedback applications. ",
"We can also use these measures to control oscillations, or control some Brain Machine Interfaces (BMIs). ",
"But first, a little more about the tecnology we are using to measure and vizualize these signals. "
],
"hardwareHead": "EEG Hardware",
"hardware1": [
"Because the brain has a great deal of salty water in it, it conducts electricity. ",
"This electrical field gets smeared by the slightly electrically resistive skull and scalp. ",
"Therefore the signal on the outside of the head has very little spatial information about where the signal came from. ",
"Even worse, any potential measured on the head could have an infinite number of dipole configurations inside the head creating it. ",
"Nonetheless, there are still difference in voltage between different parts of the head that may be interesting. "
],
"hardware2": [
"To measure the spatial distribution of the voltage signals, EEG is traditionally placed in a regular grid of electrode locations covering the surface of the head. ",
"Each location is given a name, with the letter indicating the location of the head (F-Frontal; C-Central; P-Parietal; T-Temporal; O-Occipital; Fp-Fronto-polar). ",
"The suffix has a z if along the midline, odd numbers over the left hemisphere, and even over the right. ",
"Numbers start along the midline and get larger for more lateral sites on the head. "
],
"hardware3": [
"Voltage is electrical potential, and like a battery, is measured as the difference between two locations. ",
"In the case of EEG, we use a reference electrode, shown here in black, to compare each of the other electrode locations against. ",
"The EEG device must therefore measure the difference in voltage at each of its sensors compared to some reference location. ",
"It must then amplify this very small signal, and convert this voltage to some signal that can be saved by a computer (digitization), and in the case of wireless EEG, transmit the signal. ",
"A computer must then receive this signal, and display it, process it, or save it for later analysis. "
],
"hardware4": [
"The amplification and digitization turns the continuous voltage into a digitized signal. ",
"This signal now has descrete time steps and descrete difference in voltage. ",
"The hardware's sampling rate controls how many samples of voltage per second (in Hz) are recorded. For example Muse 2 records 256 samples per second. ",
"The digitization's bit depth, or how many memory bits are used to represent each voltage value, influence the smallest change in voltage that a system can measure. ",
"Finally, since there are multiple electrode locations, these individual signals need to be digitized quickly one after another each recording cycle, this is called multiplexing. "
],
"hardware5": [
"One important consideration is the electrical conductivity between the head and the sensor. ",
"An electrode is a conductive piece of material that takes the voltage difference between locations on the head and transmits it along a wire to the amplifier/digitizer. ",
"The signal will therefore be greatly affected by the conductivity of the electrode to head connection. ",
"The inverse of conductivity we call electrical resistance, and since the EEG oscillates like an alternating current power source, we call this impedance. ",
"Notice that the muse uses two different types of sensor material, gold on the forehead, and conductive rubber behind the ears. "
],
"museHead": "Interaxon Muse EEG",
"muse1": [
"A decade ago, before the revolution in wireless and battery powered electronics, EEG devices were large and combersome. ",
"EEG devices reqired large amplifiers and digitizers, with dedicated power supplies, and desktop computers for data recording and analysis. ",
"Computing limitations limited live data processing and experimentation. ",
"Within the last decade, a series of new consumer focused EEG devices have been devleoped, drastically reducing the price and portability of the technology. ",
"One of the most common is the Muse and Muse 2 created by Toronto based Interaxon Inc. "
],
"muse2": [
"The Muse is sold as an interactive mediation device, for under 300$ US. ",
"Researchers have compared the signals with traditional expensive EEG devices and found very positive results. ",
"Therefore the muse makes for an excellent teaching tool to integrate real time brain measurement into the classroom. ",
"The muse records EEG data at 256 Hz, from four electrode locations shown here: "
],
"muse3": [
"In the subsequent modules in this EEGEdu tutorial, you will use the live data from these four electrode locations. "
],
"signalHead": "EEG Signal",
"signal1": [
"So now you have some background on how this electrical signal from your brain is generated. ",
"This particular signal is from behind your left ear, Electrode 1 at TP9. ",
"As the amplitude of the noise in the signal decreases, the line should get darker. "
],
"signal2": [
"After playing around with this live signal you are ready to move onto some modules, select one from the menu above. "
],
"creditsHead": "Credits",
"credits1": [
"EEGEdu is an open source collaborative project with NeurotechX's "
],
"credits2": [
"This is also created in collaboration with "
],
"credits3": [
"This online tutorial is made using a Muse connection by Web Bluetooth using "
],
"credits4": [
"by Uri Shaked who has an excellent introduction to EEG "
],
"credits5": [
"The data is processed using Neurosity's "
],
"credits6": [
"by Alex Castillo who also has an excellent post about EEG and the web called "
],
"xlabel": "Time (msec)",
"ylabel": "Voltage (\u03BCV)"
}
================================================
FILE: src/components/PageSwitcher/components/EEGEduPredict/EEGEduPredict.js
================================================
import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { TextContainer, Card, Stack, Button, ButtonGroup } from "@shopify/polaris";
import { Subject } from "rxjs";
import { zipSamples } from "muse-js";
import {
bandpassFilter,
epoch,
fft,
sliceFFT
} from "@neurosity/pipes";
import { chartStyles } from "../chartOptions";
import * as generalTranslations from "../translations/en";
import * as specificTranslations from "./translations/en";
import P5Wrapper from 'react-p5-wrapper';
import sketchPredict from './sketchPredictSound';
import ml5 from 'ml5'
let knnClassifier = ml5.KNNClassifier();
export function getSettings() {
return {
cutOffLow: 2,
cutOffHigh: 20,
interval: 256,
bins: 256,
sliceFFTLow: 1,
sliceFFTHigh: 30,
duration: 512,
srate: 256,
name: 'Predict'
}
};
export function buildPipe(Settings) {
if (window.subscriptionPredict) window.subscriptionPredict.unsubscribe();
window.pipePredict$ = null;
window.multicastPredict$ = null;
window.subscriptionPredict = null;
// Build Pipe
window.pipePredict$ = zipSamples(window.source.eegReadings$).pipe(
bandpassFilter({
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
nbChannels: window.nchans }),
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.multicastPredict$ = window.pipePredict$.pipe(
multicast(() => new Subject())
);
}
export function setup(setData, Settings) {
console.log("Subscribing to " + Settings.name);
if (window.multicastPredict$) {
window.subscriptionPredict = window.multicastPredict$.subscribe(data => {
setData(predictData => {
Object.values(predictData).forEach((channel, index) => {
channel.datasets[0].data = data.psd[index];
channel.xLabels = data.freqs;
});
return {
ch0: predictData.ch0,
ch1: predictData.ch1,
ch2: predictData.ch2,
ch3: predictData.ch3,
ch4: predictData.ch4
};
});
});
window.multicastPredict$.connect();
console.log("Subscribed to " + Settings.name);
}
}
export function renderModule(channels) {
function renderCharts() {
return Object.values(channels.data).map((channel, index) => {
if (index === 0) {
if (channel.datasets[0].data) {
window.psd = channel.datasets[0].data;
window.freqs = channel.xLabels;
if (channel.xLabels) {
window.bins = channel.xLabels.length;
}
}
return null
} else {
return null
}
});
}
return (
<Card title={specificTranslations.title}>
<Card.Section>
<Stack>
<TextContainer>
<p>{specificTranslations.description}</p>
</TextContainer>
</Stack>
<div style={chartStyles.wrapperStyle.style}>{renderCharts()}</div>
</Card.Section>
</Card>
);
}
export function renderSliders(setData, setSettings, status, Settings) {
return null
}
// Classification algorithm (using renderRecord function)
window.exampleCounts = {A: 0, B: 0, C: 0};
window.thisLabel = 'A';
window.confidences = {A: 1, B: 0, C: 0};
window.isPredicting = false;
window.enoughLabels = false;
export function renderRecord(recordPopChange, status) {
// Adds example from current incoming psd
function addExample (label) {
if (window.psd) {
knnClassifier.addExample(window.psd, label);
window.exampleCounts[label]++;
const numLabels = knnClassifier.getNumLabels();
if (numLabels === 3) {
window.enoughLabels = true;
}
}
}
// Classifies current incoming psd and outputs results
function classify () {
window.isPredicting = true;
knnClassifier.classify(window.psd, gotResults)
}
// callback from classify to assign results to window and recurse
function gotResults(err, result) {
if (result.confidencesByLabel) {
window.confidences = result.confidencesByLabel;
if (result.label) {
switch (result.label) {
case 'A':
window.thisLabel = 'A';
break;
case 'B':
window.thisLabel = 'B';
break;
case 'C':
window.thisLabel = 'C';
break;
default:
console.lo
gitextract_dr3zp5xw/
├── .firebaserc
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows/
│ └── workflow.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── app.yaml
├── catalogue.json
├── firebase.json
├── package.json
├── public/
│ ├── index.html
│ └── manifest.json
└── src/
├── components/
│ ├── App/
│ │ ├── App.js
│ │ └── translations/
│ │ └── en.json
│ └── PageSwitcher/
│ ├── PageSwitcher.js
│ ├── components/
│ │ ├── EEGEduAlpha/
│ │ │ ├── EEGEduAlpha.js
│ │ │ ├── sketchFixation.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduAnimate/
│ │ │ ├── EEGEduAnimate.js
│ │ │ ├── sketchBands.js
│ │ │ ├── sketchCube.js
│ │ │ ├── sketchDraw.js
│ │ │ ├── sketchFlock.js
│ │ │ ├── sketchFlock3D.js
│ │ │ ├── sketchTone.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduBands/
│ │ │ ├── EEGEduBands.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduEvoked/
│ │ │ ├── EEGEduEvoked.js
│ │ │ ├── sketchEvoked.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduHeartRaw/
│ │ │ ├── EEGEduHeartRaw.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduHeartSpectra/
│ │ │ ├── EEGEduHeartSpectra.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduIntro/
│ │ │ ├── EEGEduIntro.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduPredict/
│ │ │ ├── EEGEduPredict.js
│ │ │ ├── sketchPredict.js
│ │ │ ├── sketchPredictSound.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduRaw/
│ │ │ ├── EEGEduRaw.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduSpectra/
│ │ │ ├── EEGEduSpectra.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduSpectro/
│ │ │ ├── EEGEduSpectro.js
│ │ │ ├── sketchSpectro.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── EEGEduSsvep/
│ │ │ ├── EEGEduSsvep.js
│ │ │ ├── sketchFlashFast.js
│ │ │ ├── sketchFlashSlow.js
│ │ │ └── translations/
│ │ │ └── en.json
│ │ ├── chartOptions.js
│ │ └── translations/
│ │ └── en.json
│ ├── translations/
│ │ └── en.json
│ └── utils/
│ ├── chartUtils.js
│ └── mockMuseEEG.js
└── index.js
SYMBOL INDEX (95 symbols across 28 files)
FILE: src/components/App/App.js
function App (line 7) | function App() {
FILE: src/components/PageSwitcher/PageSwitcher.js
function PageSwitcher (line 36) | function PageSwitcher() {
FILE: src/components/PageSwitcher/components/EEGEduAlpha/EEGEduAlpha.js
function getSettings (line 30) | function getSettings() {
function buildPipe (line 50) | function buildPipe(Settings) {
function setup (line 79) | function setup(setData, Settings) {
function renderModule (line 105) | function renderModule(channels) {
function renderSliders (line 229) | function renderSliders(setData, setSettings, status, Settings) {
function renderRecord (line 314) | function renderRecord(recordPopChange, recordPop, status, Settings, reco...
function saveToCSV (line 440) | function saveToCSV(Settings, condition) {
FILE: src/components/PageSwitcher/components/EEGEduAlpha/sketchFixation.js
function sketchFixation (line 1) | function sketchFixation (p) {
FILE: src/components/PageSwitcher/components/EEGEduAnimate/EEGEduAnimate.js
function getSettings (line 31) | function getSettings () {
function buildPipe (line 43) | function buildPipe(Settings) {
function setup (line 71) | function setup(setData, Settings) {
function renderModule (line 103) | function renderModule(channels) {
function renderSliders (line 232) | function renderSliders(setData, setSettings, status, Settings) {
FILE: src/components/PageSwitcher/components/EEGEduAnimate/sketchBands.js
function sketchBands (line 1) | function sketchBands (p) {
FILE: src/components/PageSwitcher/components/EEGEduAnimate/sketchCube.js
function sketchCube (line 1) | function sketchCube (p) {
FILE: src/components/PageSwitcher/components/EEGEduAnimate/sketchDraw.js
function sketchDraw (line 1) | function sketchDraw (p) {
FILE: src/components/PageSwitcher/components/EEGEduAnimate/sketchFlock.js
function sketchFlock (line 4) | function sketchFlock (p) {
FILE: src/components/PageSwitcher/components/EEGEduAnimate/sketchFlock3D.js
function sketchFlock3D (line 3) | function sketchFlock3D (p) {
FILE: src/components/PageSwitcher/components/EEGEduAnimate/sketchTone.js
function sketchTone (line 5) | function sketchTone (p) {
FILE: src/components/PageSwitcher/components/EEGEduBands/EEGEduBands.js
function getSettings (line 27) | function getSettings () {
function buildPipe (line 40) | function buildPipe(Settings) {
function setup (line 68) | function setup(setData, Settings) {
function renderModule (line 100) | function renderModule(channels) {
function renderSliders (line 280) | function renderSliders(setData, setSettings, status, Settings) {
function renderRecord (line 341) | function renderRecord(recordPopChange, recordPop, status, Settings, setS...
function saveToCSV (line 432) | function saveToCSV(Settings) {
FILE: src/components/PageSwitcher/components/EEGEduEvoked/EEGEduEvoked.js
function getSettings (line 27) | function getSettings () {
function buildPipe (line 39) | function buildPipe(Settings) {
function setup (line 65) | function setup(setData, Settings) {
function renderModule (line 93) | function renderModule(channels) {
function renderSliders (line 114) | function renderSliders(setData, setSettings, status, Settings) {
function renderRecord (line 175) | function renderRecord(recordPopChange, recordPop, status, Settings, setS...
function saveToCSV (line 251) | function saveToCSV(Settings) {
FILE: src/components/PageSwitcher/components/EEGEduEvoked/sketchEvoked.js
function sketchEvoked (line 1) | function sketchEvoked (p) {
FILE: src/components/PageSwitcher/components/EEGEduHeartRaw/EEGEduHeartRaw.js
function getSettings (line 25) | function getSettings () {
function buildPipe (line 36) | function buildPipe(Settings) {
function setup (line 62) | function setup(setData, Settings) {
function renderModule (line 85) | function renderModule(channels) {
function renderRecord (line 249) | function renderRecord(recordPopChange, recordPop, status, Settings) {
function saveToCSV (line 351) | function saveToCSV(Settings) {
FILE: src/components/PageSwitcher/components/EEGEduHeartSpectra/EEGEduHeartSpectra.js
function getSettings (line 27) | function getSettings() {
function buildPipe (line 43) | function buildPipe(Settings) {
function setup (line 72) | function setup(setData, Settings) {
function renderModule (line 94) | function renderModule(channels) {
function renderSliders (line 335) | function renderSliders(setData, setSettings, status, Settings) {
function renderRecord (line 420) | function renderRecord(recordPopChange, recordPop, status, Settings, setS...
function saveToCSV (line 558) | function saveToCSV(Settings) {
function indexOfMax (line 600) | function indexOfMax(arr) {
FILE: src/components/PageSwitcher/components/EEGEduIntro/EEGEduIntro.js
function getSettings (line 22) | function getSettings () {
function buildPipe (line 33) | function buildPipe(Settings) {
function setup (line 59) | function setup(setData, Settings) {
function renderModule (line 84) | function renderModule(channels) {
FILE: src/components/PageSwitcher/components/EEGEduPredict/EEGEduPredict.js
function getSettings (line 29) | function getSettings() {
function buildPipe (line 43) | function buildPipe(Settings) {
function setup (line 72) | function setup(setData, Settings) {
function renderModule (line 98) | function renderModule(channels) {
function renderSliders (line 131) | function renderSliders(setData, setSettings, status, Settings) {
function renderRecord (line 143) | function renderRecord(recordPopChange, status) {
FILE: src/components/PageSwitcher/components/EEGEduPredict/sketchPredict.js
function sketchPredict (line 1) | function sketchPredict (p) {
FILE: src/components/PageSwitcher/components/EEGEduPredict/sketchPredictSound.js
function sketchPredict (line 5) | function sketchPredict (p) {
FILE: src/components/PageSwitcher/components/EEGEduRaw/EEGEduRaw.js
function getSettings (line 27) | function getSettings () {
function buildPipe (line 39) | function buildPipe(Settings) {
function setup (line 65) | function setup(setData, Settings) {
function renderModule (line 92) | function renderModule(channels) {
function renderSliders (line 402) | function renderSliders(setData, setSettings, status, Settings) {
function renderRecord (line 463) | function renderRecord(recordPopChange, recordPop, status, Settings, setS...
function saveToCSV (line 524) | function saveToCSV(Settings) {
FILE: src/components/PageSwitcher/components/EEGEduSpectra/EEGEduSpectra.js
function getSettings (line 27) | function getSettings() {
function buildPipe (line 44) | function buildPipe(Settings) {
function setup (line 73) | function setup(setData, Settings) {
function renderModule (line 99) | function renderModule(channels) {
function renderSliders (line 306) | function renderSliders(setData, setSettings, status, Settings) {
function renderRecord (line 391) | function renderRecord(recordPopChange, recordPop, status, Settings, setS...
function saveToCSV (line 502) | function saveToCSV(Settings) {
FILE: src/components/PageSwitcher/components/EEGEduSpectro/EEGEduSpectro.js
function getSettings (line 25) | function getSettings () {
function buildPipe (line 39) | function buildPipe(Settings) {
function setup (line 67) | function setup(setData, Settings) {
function renderModule (line 93) | function renderModule(channels) {
function renderSliders (line 147) | function renderSliders(setData, setSettings, status, Settings) {
FILE: src/components/PageSwitcher/components/EEGEduSpectro/sketchSpectro.js
function sketchSpectro (line 3) | function sketchSpectro (p) {
FILE: src/components/PageSwitcher/components/EEGEduSsvep/EEGEduSsvep.js
function getSettings (line 30) | function getSettings() {
function buildPipe (line 45) | function buildPipe(Settings) {
function setup (line 74) | function setup(setData, Settings) {
function renderModule (line 101) | function renderModule(channels) {
function renderSliders (line 167) | function renderSliders(setData, setSettings, status, Settings) {
function renderRecord (line 252) | function renderRecord(recordPopChange, recordPop, status, Settings, reco...
function saveToCSV (line 341) | function saveToCSV(Settings, condition) {
FILE: src/components/PageSwitcher/components/EEGEduSsvep/sketchFlashFast.js
function sketchFlash (line 1) | function sketchFlash (p) {
FILE: src/components/PageSwitcher/components/EEGEduSsvep/sketchFlashSlow.js
function sketchFlash (line 1) | function sketchFlash (p) {
FILE: src/components/PageSwitcher/utils/chartUtils.js
function customCount (line 2) | function customCount(start, end, step = 1) {
function average (line 10) | function average(data){
function generateXTics (line 22) | function generateXTics(srate, duration, reverse = true) {
function standardDeviation (line 45) | function standardDeviation(values){
Condensed preview — 61 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (286K chars).
[
{
"path": ".firebaserc",
"chars": 48,
"preview": "{\n \"projects\": {\n \"default\": \"eegedu\"\n }\n}\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 834,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 595,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
},
{
"path": ".github/workflows/workflow.yml",
"chars": 1385,
"preview": "# .github/workflows/nodejs.yml\nname: Build and Deploy to Firebase\n\non: \n push: # Run on Push Requests\n branches:\n "
},
{
"path": ".gitignore",
"chars": 312,
"preview": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n\n# testing\n/cov"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3350,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "CONTRIBUTING.md",
"chars": 1445,
"preview": "\n🎉 Thanks for thinking about making a contribution! 🎉\n\nThe following is a set of guidelines for contributing to `EEGEdu`"
},
{
"path": "LICENSE",
"chars": 1071,
"preview": "MIT License\n\nCopyright (c) 2020 Kyle Mathewson\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "README.md",
"chars": 9541,
"preview": "# EEGEdu\n\n<p align=\"center\"> \n<a href=\"https://github.com/kylemath/EEGEdu/blob/master/LICENSE\" target=\"_blank\">\n<img src"
},
{
"path": "app.yaml",
"chars": 32,
"preview": "runtime: nodejs10\nenv: standard\n"
},
{
"path": "catalogue.json",
"chars": 381,
"preview": "{\n \"id\": \"EEGEdu\",\n \"title\": \"EEGEdu\",\n \"oneLiner\": \"An interactive website to learn about brain waves\",\n \"demoUrl\":"
},
{
"path": "firebase.json",
"chars": 359,
"preview": "{\n \"hosting\": {\n \"headers\": [\n { \"source\":\"/service-worker.js\", \"headers\": [{\"key\": \"Cache-Control\", \"value\": \""
},
{
"path": "package.json",
"chars": 1173,
"preview": "{\n \"engines\": {\n \"npm\": \"5.x\",\n \"node\": \"10.x\"\n },\n \"name\": \"eegedu\",\n \"version\": \"0.1.0\",\n \"private\": true,\n"
},
{
"path": "public/index.html",
"chars": 1687,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initia"
},
{
"path": "public/manifest.json",
"chars": 309,
"preview": "{\n \"short_name\": \"EEGEdu\",\n \"name\": \"EEGedu Application\",\n \"icons\": [\n {\n \"src\": \"favicon.ico\",\n \"sizes\""
},
{
"path": "src/components/App/App.js",
"chars": 865,
"preview": "import React from \"react\";\nimport { PageSwitcher } from \"../PageSwitcher/PageSwitcher\";\nimport { AppProvider, Card, Page"
},
{
"path": "src/components/App/translations/en.json",
"chars": 1359,
"preview": "{\n \"title\": \"EEGEdu\",\n \"subtitle\": [\n \"Welcome to the EEGEdu live EEG tutorial. \",\n \"This tutorial will help you"
},
{
"path": "src/components/PageSwitcher/PageSwitcher.js",
"chars": 15730,
"preview": "import React, { useState, useCallback } from \"react\";\nimport { MuseClient } from \"muse-js\";\nimport { Select, Card, Stack"
},
{
"path": "src/components/PageSwitcher/components/EEGEduAlpha/EEGEduAlpha.js",
"chars": 17642,
"preview": "import React from \"react\";\nimport { catchError, multicast } from \"rxjs/operators\";\n\nimport { TextContainer, Card, Stack,"
},
{
"path": "src/components/PageSwitcher/components/EEGEduAlpha/sketchFixation.js",
"chars": 354,
"preview": "export default function sketchFixation (p) {\n\n p.setup = function () {\n p.createCanvas(300, 300);\n };\n\n p.windowRes"
},
{
"path": "src/components/PageSwitcher/components/EEGEduAlpha/translations/en.json",
"chars": 662,
"preview": "{\n \"title\": \"Eyes open vs. Eyes Closed Alpha Experiment\",\n \"description\": [\n \"In the next demo we run our first exp"
},
{
"path": "src/components/PageSwitcher/components/EEGEduAnimate/EEGEduAnimate.js",
"chars": 8792,
"preview": "import React, { useState, useCallback } from \"react\";\nimport { catchError, multicast } from \"rxjs/operators\";\n\nimport { "
},
{
"path": "src/components/PageSwitcher/components/EEGEduAnimate/sketchBands.js",
"chars": 1554,
"preview": "export default function sketchBands (p) {\n let delta = 0;\n let theta = 0;\n let alpha = 0;\n let beta = 0;\n let gamma"
},
{
"path": "src/components/PageSwitcher/components/EEGEduAnimate/sketchCube.js",
"chars": 826,
"preview": "export default function sketchCube (p) {\n let delta = 0;\n // let theta = 0;\n // let alpha = 0;\n // let beta = 0;\n /"
},
{
"path": "src/components/PageSwitcher/components/EEGEduAnimate/sketchDraw.js",
"chars": 1070,
"preview": "export default function sketchDraw (p) {\n let delta = 0;\n let theta = 0;\n let alpha = 0;\n let beta = 0;\n let gamma "
},
{
"path": "src/components/PageSwitcher/components/EEGEduAnimate/sketchFlock.js",
"chars": 6349,
"preview": "import p5 from \"p5\";\nimport \"p5/lib/addons/p5.sound\";\n\nexport default function sketchFlock (p) {\n\n let alpha = 0;\n let"
},
{
"path": "src/components/PageSwitcher/components/EEGEduAnimate/sketchFlock3D.js",
"chars": 20827,
"preview": "import p5 from 'p5';\n\nexport default function sketchFlock3D (p) {\n \n const flock = []; // Array of boids\n let depth ="
},
{
"path": "src/components/PageSwitcher/components/EEGEduAnimate/sketchTone.js",
"chars": 1631,
"preview": "import p5 from \"p5\";\nimport \"p5/lib/addons/p5.sound\";\n\n\nexport default function sketchTone (p) {\n let delta = 0;\n let "
},
{
"path": "src/components/PageSwitcher/components/EEGEduAnimate/translations/en.json",
"chars": 350,
"preview": "{\n \"title\": \"P5js integration\",\n \"description\": [\n \"In the next demo we look at the traditional frequency bands. \","
},
{
"path": "src/components/PageSwitcher/components/EEGEduBands/EEGEduBands.js",
"chars": 16459,
"preview": "import React from \"react\";\nimport { catchError, multicast } from \"rxjs/operators\";\n\nimport { TextContainer, Card, Stack,"
},
{
"path": "src/components/PageSwitcher/components/EEGEduBands/translations/en.json",
"chars": 105,
"preview": "{\n \"title\": \"Frequency Bands Data\",\n \"xlabel\": \"Frequency (Hz)\",\n \"ylabel\": \"Power (\\u03BCV\\u00B2)\"\n}\n"
},
{
"path": "src/components/PageSwitcher/components/EEGEduEvoked/EEGEduEvoked.js",
"chars": 10325,
"preview": "import React from \"react\";\nimport { catchError, multicast } from \"rxjs/operators\";\nimport { Subject, timer } from \"rxjs\""
},
{
"path": "src/components/PageSwitcher/components/EEGEduEvoked/sketchEvoked.js",
"chars": 1622,
"preview": "export default function sketchEvoked (p) {\n\n let x = 0;\n let thisRand = 0.5; //for random choice of target type\n let tar"
},
{
"path": "src/components/PageSwitcher/components/EEGEduEvoked/translations/en.json",
"chars": 696,
"preview": "{\n \"title\": \"Stimulus Evoked Event-related potential (ERP)\",\n \"description\": [\n \t\"The electrical activity evoked by i"
},
{
"path": "src/components/PageSwitcher/components/EEGEduHeartRaw/EEGEduHeartRaw.js",
"chars": 14043,
"preview": "import React from \"react\";\nimport { catchError, multicast, take } from \"rxjs/operators\";\nimport { Subject } from \"rxjs\""
},
{
"path": "src/components/PageSwitcher/components/EEGEduHeartRaw/translations/en.json",
"chars": 1168,
"preview": "{\n \"title\": \"Electrocardiogram (Heart Beats)\",\n \"description\": [\n \"As a first introduction to measurement of electr"
},
{
"path": "src/components/PageSwitcher/components/EEGEduHeartSpectra/EEGEduHeartSpectra.js",
"chars": 24538,
"preview": "import React from \"react\";\nimport { catchError, multicast } from \"rxjs/operators\";\n\nimport { TextContainer, Card, Stack,"
},
{
"path": "src/components/PageSwitcher/components/EEGEduHeartSpectra/translations/en.json",
"chars": 121,
"preview": "{\n \"title\": \"Heart Rate (Beats per minute)\",\n \"xlabel\": \"Heart Frequency (BPM)\",\n \"ylabel\": \"Power (\\u03BCV\\u00B2)\"\n}"
},
{
"path": "src/components/PageSwitcher/components/EEGEduIntro/EEGEduIntro.js",
"chars": 10086,
"preview": "import React from \"react\";\nimport { catchError, multicast } from \"rxjs/operators\";\nimport { Subject } from \"rxjs\";\n\nimpo"
},
{
"path": "src/components/PageSwitcher/components/EEGEduIntro/translations/en.json",
"chars": 9352,
"preview": "{\n \"title\": \"Introduction\",\n \"intro1\": [\n \"Below you are now looking at an live measurement of the electrical poten"
},
{
"path": "src/components/PageSwitcher/components/EEGEduPredict/EEGEduPredict.js",
"chars": 6880,
"preview": "import React from \"react\";\nimport { catchError, multicast } from \"rxjs/operators\";\n\nimport { TextContainer, Card, Stack,"
},
{
"path": "src/components/PageSwitcher/components/EEGEduPredict/sketchPredict.js",
"chars": 1026,
"preview": "export default function sketchPredict (p) {\n\n let label;\n let confidence;\n\n p.setup = function () {\n p.createCanva"
},
{
"path": "src/components/PageSwitcher/components/EEGEduPredict/sketchPredictSound.js",
"chars": 1987,
"preview": "import p5 from \"p5\";\nimport \"p5/lib/addons/p5.sound\";\n\n\n export default function sketchPredict (p) {\n\n let label;\n let"
},
{
"path": "src/components/PageSwitcher/components/EEGEduPredict/translations/en.json",
"chars": 766,
"preview": "{\n \"title\": \"Predict brain states with a trained classifier\",\n \"description\": [\n \"In the next module we will train "
},
{
"path": "src/components/PageSwitcher/components/EEGEduRaw/EEGEduRaw.js",
"chars": 22170,
"preview": " import React from \"react\";\nimport { catchError, multicast } from \"rxjs/operators\";\nimport { Subject, timer } from \"rxjs"
},
{
"path": "src/components/PageSwitcher/components/EEGEduRaw/translations/en.json",
"chars": 384,
"preview": "{\n \"title\": \"Raw data\",\n \"description\": \"First we look at the raw voltage signals coming from each of the four sensors"
},
{
"path": "src/components/PageSwitcher/components/EEGEduSpectra/EEGEduSpectra.js",
"chars": 20881,
"preview": "import React from \"react\";\nimport { catchError, multicast } from \"rxjs/operators\";\n\nimport { TextContainer, Card, Stack,"
},
{
"path": "src/components/PageSwitcher/components/EEGEduSpectra/translations/en.json",
"chars": 579,
"preview": "{\n \"title\": \"Frequency Domain Data\",\n \"description\": \"In the next demo we will look at the same raw EEG data but this "
},
{
"path": "src/components/PageSwitcher/components/EEGEduSpectro/EEGEduSpectro.js",
"chars": 6699,
"preview": "import React from \"react\";\nimport { catchError, multicast } from \"rxjs/operators\";\n\nimport { Card, Stack, TextContainer,"
},
{
"path": "src/components/PageSwitcher/components/EEGEduSpectro/sketchSpectro.js",
"chars": 1098,
"preview": "import \"p5/lib/addons/p5.sound\";\n\nexport default function sketchSpectro (p) {\n \n let spectrum;\n let binCount;\n let s"
},
{
"path": "src/components/PageSwitcher/components/EEGEduSpectro/translations/en.json",
"chars": 638,
"preview": "{\n \"title\": \"Spectrogram (Spectra over time)\",\n \"description\": [\n \"Back to the full spectra, we can also look at ho"
},
{
"path": "src/components/PageSwitcher/components/EEGEduSsvep/EEGEduSsvep.js",
"chars": 11790,
"preview": "import React from \"react\";\nimport { catchError, multicast } from \"rxjs/operators\";\n\nimport { TextContainer, Card, Stack,"
},
{
"path": "src/components/PageSwitcher/components/EEGEduSsvep/sketchFlashFast.js",
"chars": 784,
"preview": "export default function sketchFlash (p) {\n\n\n const freq = 11; \n let x = 0;\n let startTime = 0;\n let newOnset = true;"
},
{
"path": "src/components/PageSwitcher/components/EEGEduSsvep/sketchFlashSlow.js",
"chars": 783,
"preview": "export default function sketchFlash (p) {\n\n\n const freq = 4; \n let x = 0;\n let startTime = 0;\n let newOnset = true;\n"
},
{
"path": "src/components/PageSwitcher/components/EEGEduSsvep/translations/en.json",
"chars": 790,
"preview": "{\n \"title\": \"Steady-State Visual Evoked Potential (SSVEP) Experiment\",\n \"description\": [\n \"In the next demo we run "
},
{
"path": "src/components/PageSwitcher/components/chartOptions.js",
"chars": 1071,
"preview": "import * as generalTranslations from \"./translations/en\";\n\nexport const chartStyles = {\n wrapperStyle: {\n display: \""
},
{
"path": "src/components/PageSwitcher/components/translations/en.json",
"chars": 347,
"preview": "{\n \"connect\": \"Connect Muse Headband\",\n \"connectMock\": \"Connect Mock Data\",\n \"connecting\": \"Connecting to Muse\",\n \"c"
},
{
"path": "src/components/PageSwitcher/translations/en.json",
"chars": 677,
"preview": "{\n \"title\": \"Choose your Module\",\n \"types\": {\n \t\"intro\": \"1. Introduction\",\n \"heartRaw\": \"2. Electrocardiogram (He"
},
{
"path": "src/components/PageSwitcher/utils/chartUtils.js",
"chars": 1287,
"preview": "// Function to count by n to something\nexport function customCount(start, end, step = 1) {\n const len = Math.floor((end"
},
{
"path": "src/components/PageSwitcher/utils/mockMuseEEG.js",
"chars": 650,
"preview": "import { customCount } from './chartUtils'\n\nconst { interval, from } = require('rxjs');\nconst { map, flatMap } = require"
},
{
"path": "src/index.js",
"chars": 165,
"preview": "import React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport { App } from \"./components/App/App\";\n\nReactDOM.rende"
}
]
About this extraction
This page contains the full source code of the kylemath/EEGEdu GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 61 files (264.2 KB), approximately 67.4k tokens, and a symbol index with 95 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.