122 Commits

Autor SHA1 Mensagem Data
Kyle Mathewson d819ef61ea Merge pull request #114 from kylemath/evoked2
removed settings
2020-01-06 01:59:28 -07:00
Kyle Mathewson 4ed59b96ee removed settings 2020-01-06 01:58:54 -07:00
Kyle Mathewson 0a0ec6eaab Merge pull request #110 from kylemath/evoked
[ready to deploy] - Markers locked to P5 stimuli logged into Raw csv file
2020-01-06 01:51:09 -07:00
Kyle Mathewson a0ca1deefb back to intro default 2020-01-05 21:31:42 -07:00
Kyle Mathewson 8f72399b15 frameRate for SSVEP 2020-01-05 20:58:49 -07:00
Kyle Mathewson 1c00ac0805 created oddball task, response markers for red circle spacebar or tap screen 2020-01-05 20:57:38 -07:00
Kyle Mathewson b57d116a76 Update README.md 2020-01-05 11:33:27 -07:00
Kyle Mathewson 6d4812c529 added slack badge to neurotechx 2020-01-05 11:24:03 -07:00
Kyle Mathewson 2cb1e29ce1 Merge pull request #112 from kylemath/add-code-of-conduct-1
Create CODE_OF_CONDUCT.md
2020-01-05 11:08:46 -07:00
Kory de500e378a Update issue templates 2020-01-05 12:35:03 -05:00
Kory bd702ea138 Create CODE_OF_CONDUCT.md
Adding a code of conduct for the repository.
2020-01-05 12:34:15 -05:00
Kory 713ed133c9 Update README.md with Code Pointers
Adding code pointers from the Tutorial listing.
2020-01-05 12:26:18 -05:00
Kory 96782b1e53 Update package.json to handle fatal error on build
As per here: https://stackoverflow.com/questions/55613789/how-to-fix-fatal-error-ineffective-mark-compacts-near-heap-limit-allocation-fa
2020-01-05 11:25:20 -05:00
Kory 500d7f8492 Create .env 2020-01-05 11:23:53 -05:00
Kyle Mathewson 80ba445186 removed unused imports 2020-01-05 02:17:12 -07:00
Kyle Mathewson c3e9925b2d working version of markers from p5js sketch written into a column of csv file 2020-01-05 02:06:27 -07:00
Kyle Mathewson 5ae8742861 everything ready for evoked dev in new module 2020-01-05 01:56:05 -07:00
Kyle Mathewson e5caa175f9 Update README.md 2020-01-05 00:17:10 -07:00
Kyle Mathewson 8bddd396d2 Update README.md 2020-01-05 00:12:57 -07:00
Kyle Mathewson 190f41ee88 Merge pull request #109 from kylemath/v1.0-beta
V1.0 beta
2020-01-05 00:10:24 -07:00
Kyle Mathewson 659d0583b6 Update README.md 2020-01-05 00:05:46 -07:00
Kyle Mathewson a81f544b2b Update README.md 2020-01-05 00:03:00 -07:00
Kyle Mathewson 53fd92f6e6 Update README.md 2020-01-05 00:01:19 -07:00
Kyle Mathewson c78cd910b4 Update README.md 2020-01-04 23:55:02 -07:00
Kyle Mathewson b03a23495c Update README.md 2020-01-04 23:52:14 -07:00
Kyle Mathewson 91de383110 Update README.md 2020-01-04 23:49:50 -07:00
Kyle Mathewson 0dc1348425 Update README.md 2020-01-04 23:49:13 -07:00
Kyle Mathewson d97ac44899 Update README.md 2020-01-04 23:47:59 -07:00
Kyle Mathewson 2d40d6c5fd Update README.md 2020-01-04 23:47:00 -07:00
Kyle Mathewson bce31eb179 Add files via upload 2020-01-04 23:45:09 -07:00
Kyle Mathewson 1aa81a9109 Update README.md 2020-01-04 23:33:25 -07:00
Kyle Mathewson 5a9321c31e added github link to credits 2020-01-04 23:30:47 -07:00
Kyle Mathewson 48f7565fa6 Merge pull request #108 from kylemath/captions
added caption links to images
2020-01-04 23:15:44 -07:00
Kyle Mathewson eb620af17f added caption links to images 2020-01-04 23:15:15 -07:00
Kyle Mathewson c27826cead Update README.md 2020-01-04 22:08:48 -07:00
Kyle Mathewson d4731e69fd Update README.md 2020-01-04 22:06:04 -07:00
Kyle Mathewson 61f7f5c6dd Merge pull request #105 from kylemath/heartrate4
[ready to deploy] Finally - Heartrate4
2020-01-04 21:42:33 -07:00
Kyle Mathewson ed9697719f fixed predict button to be off when predicting 2020-01-04 21:41:40 -07:00
Kyle Mathewson 4261717119 changed name of prototype 2020-01-04 21:38:57 -07:00
Kyle Mathewson 0045fe4a7f added ml5 to yarn 2020-01-04 21:32:43 -07:00
Kyle Mathewson 727b5c64cb Merge branch 'master' into heartrate4 2020-01-04 21:31:49 -07:00
Kory a421a381e3 Merge pull request #102 from kylemath/ml5
[ready to deploy] - Working KNN classification, two training classes, live prediction of state and animation of prediction
2020-01-04 23:22:16 -05:00
Kyle Mathewson 4314eff36e removed unused imports 2020-01-04 21:11:39 -07:00
Kyle Mathewson 93c5cdfeca removed extra code in raw 2020-01-04 19:13:01 -07:00
Kyle Mathewson b727ed3016 back to introduction 2020-01-04 19:07:22 -07:00
Kyle Mathewson f3ad3d60c0 records bpm over time from single channel 2020-01-04 19:06:53 -07:00
Kyle Mathewson 05c06dfa7e fixed pipes 2020-01-04 16:55:59 -07:00
Kyle Mathewson 3a73303b7b copied all files and added two heart rate modules 2020-01-04 16:25:56 -07:00
Kyle Mathewson d5ddd66d66 default back to intro 2020-01-04 15:23:49 -07:00
Kyle Mathewson ec91ee60a5 numbered all modules in dropdown 2020-01-04 15:21:57 -07:00
Kyle Mathewson 8f8a13f519 moved the animation inside he Card with the button 2020-01-04 15:20:20 -07:00
Kyle Mathewson 14c8fb079d working button logic and answering kory comments 2020-01-04 15:14:18 -07:00
Kory d300f46b4c Update README.md for proper license link 2020-01-04 14:16:50 -05:00
Kyle Mathewson 918c4a8c8e spaces 2020-01-04 02:44:17 -07:00
Kyle Mathewson a0f1a2e988 fsd
ferge branch 'ml5' of https://github.com/kylemath/eegedu into ml5
2020-01-04 02:44:02 -07:00
Kyle Mathewson be98cbb41b typo 2020-01-04 02:42:28 -07:00
Kyle Mathewson c7b7c64cb6 Merge branch 'master' into ml5 2020-01-04 02:34:19 -07:00
Kyle Mathewson 9a241225f5 working classification binary 2020-01-04 02:33:12 -07:00
Kyle Mathewson 932b4aead2 long epochs with large intervals for maximum date uniqueness 2020-01-04 00:07:29 -07:00
Kyle Mathewson 2aec155533 working two buttons are saving single epoch psds as examples of each category 2020-01-04 00:04:03 -07:00
Kyle Mathewson 13a1844c49 Merge branch 'master' of http://github.com/kylemath/eegedu
fd the commit.
2020-01-03 21:37:19 -07:00
Kyle Mathewson 8e626f3e47 upgraded handlebars after security warning in github 2020-01-03 21:37:11 -07:00
Kyle Mathewson 3c8435798a Update README.md 2020-01-03 21:31:40 -07:00
Kyle Mathewson a2b3aa4775 Update README.md 2020-01-03 21:31:10 -07:00
Kyle Mathewson 946efe16a4 Update README.md 2020-01-03 21:30:15 -07:00
Kyle Mathewson 2b0f399868 Merge pull request #99 from kylemath/korymath-remove-service-worker
Removing service worker code as it is no longer used
2020-01-03 21:28:27 -07:00
Kyle Mathewson faede23036 Merge pull request #98 from kylemath/korymath-remove-old-dataset
Removing old stale dataset
2020-01-03 21:27:46 -07:00
Kyle Mathewson 3aecc447d2 Merge pull request #100 from kylemath/korymath-firebase-index-clean
Removing firebase TODO from index.html
2020-01-03 21:27:33 -07:00
Kyle Mathewson 2450200c7e Merge pull request #97 from kylemath/korymath-badges
Adding BADGES 📛
2020-01-03 21:27:13 -07:00
Kory e69b4ad5d1 Removing firebase TODO from index.html
We do not need to add any other libraries from firebase, thus removing this stale todo
2020-01-03 16:59:08 -05:00
Kory Mathewson 981530ab82 removing service worker as it is no longer used 2020-01-03 16:55:50 -05:00
Kory Mathewson 5f280af800 removing old stale dataset 2020-01-03 16:49:30 -05:00
Kory 6ba5a78219 Adding a PR welcome badge. 2020-01-03 16:39:06 -05:00
Kory 916f91eafa Adding BADGES 📛 2020-01-03 16:36:57 -05:00
Kory Mathewson c801e4c8dc Update LICENSE 2020-01-03 15:51:33 -05:00
Kyle Mathewson 92c5d5c8af Merge pull request #94 from kylemath/korymath-rewrite
Update README.md
2020-01-03 12:21:59 -07:00
Kyle Mathewson b8216e7ae6 Merge pull request #95 from kylemath/korymath-contribution
Update CONTRIBUTING.md
2020-01-03 12:20:44 -07:00
Kory Mathewson ca24f1d3e8 Adding manual testing run through to the README.md
Addressing Issue #78 by including a manual testing run-through to the README.md. This will help to ensure that we are not making any breaking changes to the code with new features/PRs/bigfixes.
2020-01-03 11:38:59 -05:00
Kory Mathewson fd86a5f3c2 Update CONTRIBUTING.md
Making some changes to CONTRIBUTING to make sure that we are ready to go live and invite contributions.
2020-01-03 11:13:48 -05:00
Kory Mathewson 66ef387db2 Update README.md
This is a major rewrite of the README file to make sure we are up to date for launch to the public.
2020-01-03 11:06:29 -05:00
Kyle Mathewson c918ab20c3 getting sketch ready 2020-01-03 02:33:28 -07:00
Kyle Mathewson d266fd7bc4 all renaming done 2020-01-03 02:04:26 -07:00
Kyle Mathewson f04e11df3d copied files 2020-01-03 00:26:09 -07:00
Kyle Mathewson 1c38cdb53a Update README.md 2020-01-03 00:15:26 -07:00
Kyle Mathewson 06b9a90334 Merge pull request #93 from kylemath/ssvep
Ssvep
2020-01-03 00:02:02 -07:00
Kyle Mathewson 72a8c7aa2b Merge pull request #89 from kylemath/openClosed
alpha eyes open vs closed module
2020-01-03 00:00:08 -07:00
Kyle Mathewson 43bb529cc8 added fixation cross to the modal window 2020-01-02 23:57:56 -07:00
Kyle Mathewson f3823587a1 resized for mobile compatability 2020-01-02 23:52:08 -07:00
Kyle Mathewson 0f7101eab5 added fixation cross to center of window 2020-01-02 23:38:40 -07:00
Kyle Mathewson d57ef8583d back to intro 2020-01-02 23:34:32 -07:00
Kyle Mathewson aac35a5818 minor fix 2020-01-02 23:27:09 -07:00
Kyle Mathewson 10c2cc34d3 p5js flashing disk in modal windows at two different frequencies 2020-01-02 23:18:08 -07:00
Kyle Mathewson 3afc2e5a1f Update README.md 2020-01-02 15:07:58 -07:00
Kyle Mathewson ed9f4cca32 Update README.md 2020-01-02 15:02:30 -07:00
Kyle Mathewson 8fcb2d9890 Merge pull request #91 from kylemath/cache2
trying to stop cache of website
2020-01-02 14:44:37 -07:00
Kyle Mathewson aff1112fae trying to stop cache of website 2020-01-02 14:35:50 -07:00
Kyle Mathewson 67c6ccf444 starting ssvep module 2020-01-02 12:01:48 -07:00
Kyle Mathewson b6896490e2 back to normal settings for ready to deploy 2020-01-02 08:05:07 -07:00
Kyle Mathewson 6846839196 added file 2020-01-02 08:04:24 -07:00
Kyle Mathewson b1f252c193 alpha eyes open vs closed module 2020-01-02 07:57:42 -07:00
Kyle Mathewson ae36b1c176 Merge pull request #88 from kylemath/spectrogram3
Spectrogram3
2019-12-30 23:55:44 -07:00
Kyle Mathewson fd5954f9bb bug fix 2019-12-30 18:32:18 -07:00
Kyle Mathewson c5505bde4b orbit control for 3d flock 2019-12-30 17:42:57 -07:00
Kyle Mathewson 9b19cc83c7 ready for deploy 2019-12-30 17:17:08 -07:00
Kyle Mathewson 0a8f4a364a fixed some bugs 2019-12-30 17:14:04 -07:00
Kyle Mathewson 0e47abdeaa made p5js spectrogram 2019-12-30 16:44:59 -07:00
Kyle Mathewson 319c9877c3 changing default back to intro 2019-12-30 01:22:42 -07:00
Kyle Mathewson df25bc958b fixed sliders for animation 2019-12-30 01:20:58 -07:00
Kyle Mathewson 39f365a5a0 Merge pull request #86 from kylemath/p5js2
P5js2 - new animation and some fixes
2019-12-30 01:16:24 -07:00
Kyle Mathewson 842fd11f09 Merge branch 'master' into p5js2 2019-12-30 01:15:44 -07:00
Kyle Mathewson 5ff92098c8 fixed some sizing issues on mobile, etc. 2019-12-30 01:13:35 -07:00
Kyle Mathewson a6a4dc60bd got 3dflock working 2019-12-30 00:35:40 -07:00
Kyle Mathewson 84df350dfe Merge pull request #85 from kylemath/recordClean
Clean up the record button code by moving everything into the modules
2019-12-29 19:29:00 -07:00
Kyle Mathewson 6b9b55d6d3 clean 2019-12-29 17:04:30 -07:00
Kyle Mathewson 1c9c1f48b2 removed some console logs ;) 2019-12-29 17:03:08 -07:00
Kyle Mathewson f706decc79 bug 2019-12-29 16:57:08 -07:00
Kyle Mathewson f0febed2a6 moved all record code to modules 2019-12-29 16:54:33 -07:00
Kyle Mathewson 79035ed61b working on moving record button to the module for raw 2019-12-29 16:05:32 -07:00
Kyle Mathewson 93ce70ac9f wo knoews 2019-12-28 23:14:05 -07:00
Kyle Mathewson 94637e614f added 3d flock 2019-12-28 02:20:20 -07:00
Kyle Mathewson 5dfa04ddfc changed default for dev 2019-12-28 00:46:54 -07:00
Kyle Mathewson daa0787c20 changed to white back black ships 2019-12-27 19:59:23 -07:00
52 arquivos alterados com 4728 adições e 17980 exclusões
+1
Ver Arquivo
@@ -0,0 +1 @@
GENERATE_SOURCEMAP=false
+38
Ver Arquivo
@@ -0,0 +1,38 @@
---
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.
+20
Ver Arquivo
@@ -0,0 +1,20 @@
---
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.
+76
Ver Arquivo
@@ -0,0 +1,76 @@
# 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
+18
Ver Arquivo
@@ -11,6 +11,24 @@ To start, you can find all the open issues [here](https://github.com/kylemath/EE
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.
+1 -1
Ver Arquivo
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 Kyle Mathewson
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
+146 -43
Ver Arquivo
@@ -1,100 +1,203 @@
# EEGEdu
EEGEdu is an educational website to learn about working with electroencephalogram (EEG) data. It is a teaching tool that allows for students to quickly interact with their own brain waves.
<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>
It is an interactive web page that you interact with multiple demonstrations of working with EEG data. It is partially inspired by [EEG101](https://github.com/NeuroTechX/eeg-101) but it is strictly web-based. This allows students to interact with EEG brain data without having to install any software.
This is a useful set of tools that has been inspired by multiple works that came before. Previously, others in the field have been using [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 required a bluetooth low-energey (BLE) dongle to work with Windows of Mac systems. It also required the editing of some pyglatt code to connect properly. These software are cumbersome, and serve as a barrier to entry for many students learning about EEG.
Visit [https://eegedu.com/](https://eegedu.com/]) for the live website.
<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 you started.
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.
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 one-line installations for Mac users:
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
## Install homebrew
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
# Install yarn
## Install yarn
# NOTE: this will also install Node.js if it is not already installed.
brew install yarn
# Node.js must be version 10.x for Muse interaction
# Thus, if you are getting version issues, install n and switch versions
# 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, clone the git repo and enter the folder:
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
```
You then need to install the required packages for EEGEdu
Then, you can install the required `yarn` packages for EEGEdu:
```sh
yarn install
```
## Local Development Environment
Then, you can run the *Development Environment* of EEGEdu:
Then, you can run the *Local Development Environment* of EEGEdu:
```sh
yarn start dev
```
If it is working correctly, the EEGEdu application will open in a browser window at http://localhost:3000.
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).
[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.
# Development Roadmap
We are aiming to include chances for students to interact with EEG-based brain signals. So we might break down a curriculum into 10 lessons as follows:
1. Connect + hardware
2. Biophysics + signal viewing
3. Simple evoked example
4. Frequency domain explanation
5. Raw spectra
6. Frequency bands
7. Spectrogram
8. Record data
9. BCI trainer
10. p5js demos: https://p5js.org/
# References
* https://github.com/urish/muse-js - based toolbox for interacting with muse
* https://github.com/NeuroJS/angular-muse - demo with streaming data in Angular, record button,
* https://github.com/tanvach/muse-fft - starting point react demo
* https://github.com/neurosity/eeg-pipes - easy pipable operations on eeg data from muse-js
* https://reactjs.org/ - React for web development
* https://www.chartjs.org/docs/latest/ - interactive charts
* https://github.com/urish/muse-lsl - maybe useful to stream to LSL
# Credits
`EEGEdu` - An Interactive Electrophysiology Tutorial with the Interaxon Muse brought to you by Mathewson Sons
`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
Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff
Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff
+3
Ver Arquivo
@@ -1,5 +1,8 @@
{
"hosting": {
"headers": [
{ "source":"/service-worker.js", "headers": [{"key": "Cache-Control", "value": "no-cache"}] }
],
"public": "build",
"ignore": [
"firebase.json",
BIN
Ver Arquivo
Arquivo binário não exibido.

Depois

Largura:  |  Altura:  |  Tamanho: 123 KiB

+4 -2
Ver Arquivo
@@ -13,6 +13,8 @@
"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",
@@ -25,8 +27,8 @@
"rxjs": "^6.5.3"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"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"
},
-3
Ver Arquivo
@@ -16,9 +16,6 @@
<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-app.js"></script>
<!-- TODO: Add SDKs for Firebase products that you want to use
https://firebase.google.com/docs/web/setup#available-libraries -->
<script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-analytics.js"></script>
<script>
const firebaseConfig = {
+9 -2
Ver Arquivo
@@ -1,6 +1,6 @@
import React from "react";
import { PageSwitcher } from "../PageSwitcher/PageSwitcher";
import { AppProvider, Card, Page } from "@shopify/polaris";
import { AppProvider, Card, Page, Link } from "@shopify/polaris";
import enTranslations from "@shopify/polaris/locales/en.json";
import * as translations from "./translations/en.json";
@@ -10,7 +10,14 @@ export function App() {
<Page title={translations.title} subtitle={translations.subtitle}>
<PageSwitcher />
<Card sectioned>
<p>{translations.footer}</p>
<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>
+8 -4
Ver Arquivo
@@ -2,16 +2,20 @@
"title": "EEGEdu",
"subtitle": [
"Welcome to the EEGEdu live EEG tutorial. ",
"This tutorial is designed to be used with the Muse and the Muse 2 headbands from Interaxon. ",
"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. ",
"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"
"footer": "EEGEdu - An Interactive Electrophysiology Tutorial with the Muse brought to you by Mathewson Sons. "
}
+152 -255
Ver Arquivo
@@ -1,43 +1,67 @@
import React, { useState, useCallback } from "react";
import { MuseClient } from "muse-js";
import { Select, Card, Stack, Button, ButtonGroup, Modal, TextContainer } from "@shopify/polaris";
import { saveAs } from 'file-saver';
import { take } from "rxjs/operators";
import { Select, Card, Stack, Button, ButtonGroup } from "@shopify/polaris";
import { mockMuseEEG } from "./utils/mockMuseEEG";
import { generateXTics } from "./utils/chartUtils";
import * as translations from "./translations/en.json";
import * as generalTranslations from "./components/translations/en";
import { emptyChannelData } from "./components/chartOptions";
import { emptyChannelData, emptySingleChannelData } 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 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() {
// data pulled out of multicast$
const [introData, setIntroData] = useState(emptyChannelData)
const [heartRawData, setHeartRawData] = useState(emptyChannelData);
const [heartSpectraData, setHeartSpectraData] = useState(emptySingleChannelData);
const [rawData, setRawData] = useState(emptyChannelData);
const [spectraData, setSpectraData] = useState(emptyChannelData);
const [bandsData, setBandsData] = useState(emptyChannelData);
const [animateData, setAnimateData] = useState(emptyChannelData);
const [spectroData, setSpectroData] = useState(emptyChannelData);
const [alphaData, setAlphaData] = useState(emptyChannelData);
const [ssvepData, setSsvepData] = useState(emptyChannelData);
const [evokedData, setEvokedData] = useState(emptyChannelData);
const [predictData, setPredictData] = useState(emptyChannelData);
// pipe settings
const [introSettings] = useState(funIntro.getSettings);
const [heartRawSettings] = useState(funHeartRaw.getSettings);
const [heartSpectraSettings] = useState(funHeartSpectra.getSettings);
const [rawSettings, setRawSettings] = useState(funRaw.getSettings);
const [spectraSettings, setSpectraSettings] = useState(funSpectra.getSettings);
const [bandsSettings, setBandsSettings] = useState(funBands.getSettings);
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] = useState(funEvoked.getSettings);
const [predictSettings, setPredictSettings] = useState(funPredict.getSettings);
// connection status
const [status, setStatus] = useState(generalTranslations.connect);
@@ -50,10 +74,17 @@ export function PageSwitcher() {
console.log("Switching to: " + value);
if (window.subscriptionIntro) window.subscriptionIntro.unsubscribe();
if (window.subscriptionHeartRaw) window.subscriptionHeartRaw.unsubscribe();
if (window.subscriptionHeartSpectra) window.subscriptionHeartSpectra.unsubscribe();
if (window.subscriptionRaw) window.subscriptionRaw.unsubscribe();
if (window.subscriptionSpectra) window.subscriptionSpectra.unsubscribe();
if (window.subscriptionBands) window.subscriptionBands.unsubscribe();
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
@@ -63,20 +94,39 @@ export function PageSwitcher() {
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]);
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: 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) {
@@ -84,6 +134,12 @@ export function PageSwitcher() {
case intro:
funIntro.setup(setIntroData, introSettings);
break;
case heartRaw:
funHeartRaw.setup(setHeartRawData, heartRawSettings);
break;
case heartSpectra:
funHeartSpectra.setup(setHeartSpectraData, heartSpectraSettings);
break;
case raw:
funRaw.setup(setRawData, rawSettings);
break;
@@ -91,10 +147,25 @@ export function PageSwitcher() {
funSpectra.setup(setSpectraData, spectraSettings);
break;
case bands:
funBands.setup(setBandsData, bandsSettings)
funBands.setup(setBandsData, bandsSettings);
break;
case animate:
funAnimate.setup(setAnimateData, animateSettings)
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(
@@ -103,116 +174,11 @@ export function PageSwitcher() {
}
}
function saveToCSV(value) {
const numSamplesToSave = 50;
console.log('Saving ' + numSamplesToSave + ' samples...');
var localObservable$ = null;
const dataToSave = [];
console.log('making ' + value + ' headers')
// for each module subscribe to multicast and make header
switch (value) {
case raw:
// take one sample from selected observable object for headers
localObservable$ = window.multicastRaw$.pipe(
take(1)
);
//take one sample to get header info
localObservable$.subscribe({
next(x) {
dataToSave.push(
"Timestamp (ms),",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch0_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch1_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch2_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch3_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "chAux_" + f + "ms"}) + ",",
"info",
"\n"
);
}
});
// put selected observable object into local and start taking samples
localObservable$ = window.multicastRaw$.pipe(
take(numSamplesToSave)
);
break;
case spectra:
// take one sample from selected observable object for headers
localObservable$ = window.multicastSpectra$.pipe(
take(1)
);
localObservable$.subscribe({
next(x) {
let freqs = Object.values(x.freqs);
dataToSave.push(
"Timestamp (ms),",
freqs.map(function(f) {return "ch0_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "ch1_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "ch2_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "ch3_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "chAux_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "f_" + f + "Hz"}) + "," ,
"info",
"\n"
);
}
});
// put selected observable object into local and start taking samples
localObservable$ = window.multicastSpectra$.pipe(
take(numSamplesToSave)
);
break;
case bands:
dataToSave.push(
"Timestamp (ms),",
"delta0,delta1,delta2,delta3,deltaAux,",
"theta0,theta1,theta2,theta3,thetaAux,",
"alpha0,alpha1,alpha2,alpha3,alphaAux,",
"beta0,beta1,beta2,beta3,betaAux,",
"delta0,delta1,delta2,delta3,deltaAux\n"
);
// put selected observable object into local and start taking samples
localObservable$ = window.multicastBands$.pipe(
take(numSamplesToSave)
);
break;
default:
console.log(
"Error on creating header in module: " + value
);
}
// 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, value + "_Recording.csv");
console.log('Completed');
}
});
}
async function connect() {
try {
if (window.debugWithMock) {
// Debug with Mock EEG Data
// Initialize the mockMuseEEG data stream with sampleRate
console.log("Connecting to mock data source...");
setStatus(generalTranslations.connectingMock);
window.source = {};
window.source.connectionStatus = {};
window.source.connectionStatus.value = true;
@@ -220,28 +186,19 @@ export function PageSwitcher() {
setStatus(generalTranslations.connectedMock);
} else {
// Connect with the Muse EEG Client
console.log("Connecting to data source observable...");
setStatus(generalTranslations.connecting);
window.source = new MuseClient();
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$
) {
console.log("Starting to build the data pipes from the data source...");
buildPipes(selected);
console.log("Finished building the data pipes from the data source");
subscriptionSetup(selected);
console.log("Finished subscribing to the data source");
}
} catch (err) {
setStatus(generalTranslations.connect);
@@ -257,39 +214,56 @@ export function PageSwitcher() {
switch(selected) {
case intro:
return null
case heartRaw:
return null
case heartSpectra:
return null
case raw:
return (
<Card title={selected + ' Settings'} sectioned>
{funRaw.renderSliders(setRawData, setRawSettings, status, rawSettings)}
</Card>
funRaw.renderSliders(setRawData, setRawSettings, status, rawSettings)
);
case spectra:
return (
<Card title={selected + ' Settings'} sectioned>
{funSpectra.renderSliders(setSpectraData, setSpectraSettings, status, spectraSettings)}
</Card>
funSpectra.renderSliders(setSpectraData, setSpectraSettings, status, spectraSettings)
);
case bands:
return (
<Card title={selected + ' Settings'} sectioned>
{funBands.renderSliders(setBandsData, setBandsSettings, status, bandsSettings)}
</Card>
funBands.renderSliders(setBandsData, setBandsSettings, status, bandsSettings)
);
case animate:
return (
<Card title={selected + ' Settings'} sectioned>
{funAnimate.renderSliders(setAnimateData, setAnimateSettings, status, animateSettings)}
</Card>
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 renderCharts() {
console.log("Rendering " + selected + " Component");
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:
@@ -297,147 +271,70 @@ export function PageSwitcher() {
case bands:
return <funBands.renderModule data={bandsData} />;
case animate:
return <funAnimate.renderModule
data={animateData}
/>;
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 null
case heartSpectra:
return (
funHeartSpectra.renderRecord(recordPopChange, recordPop, status, heartSpectraSettings)
)
case raw:
return (
<Card title={'Record Raw Data'} sectioned>
<Card.Section>
<p>
{"When you are recording raw data it is recommended you set the "}
{"number of sampling points between epochs onsets to be equal to the epoch duration. "}
{"This will ensure that consecutive rows of your output file are not overlapping in time."}
{"It will make the live plots appear more choppy."}
</p>
</Card.Section>
<Stack>
<ButtonGroup>
<Button
onClick={() => {
saveToCSV(selected);
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>
)
funRaw.renderRecord(recordPopChange, recordPop, status, rawSettings)
)
case spectra:
return (
<Card title={'Record Raw Data'} sectioned>
<Card.Section>
<p>
{"When you are recording raw data it is recommended you set the "}
{"number of sampling points between epochs onsets to be equal to the epoch duration. "}
{"This will ensure that consecutive rows of your output file are not overlapping in time."}
{"It will make the live plots appear more choppy."}
</p>
</Card.Section>
<Stack>
<ButtonGroup>
<Button
onClick={() => {
saveToCSV(selected);
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>
funSpectra.renderRecord(recordPopChange, recordPop, status, spectraSettings)
)
case bands:
return (
<Card title={'Record Data'} sectioned>
<Stack>
<ButtonGroup>
<Button
onClick={() => {
saveToCSV(selected);
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>
funBands.renderRecord(recordPopChange, recordPop, status, bandsSettings)
)
case animate:
return null
case spectro:
return null
case alpha:
return (
funAlpha.renderRecord(recordPopChange, recordPop, status, alphaSettings, recordTwoPopChange, recordTwoPop)
)
case ssvep:
return (
funSsvep.renderRecord(recordPopChange, recordPop, status, ssvepSettings, recordTwoPopChange, recordTwoPop)
)
case evoked:
return (
funEvoked.renderRecord(recordPopChange, recordPop, status, evokedSettings)
)
case predict:
return (
funPredict.renderRecord(status)
)
default:
console.log("Error on renderRecord.");
}
}
// Render the entire page using above functions
return (
<React.Fragment>
<Card sectioned>
@@ -0,0 +1,375 @@
import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
import { saveAs } from 'file-saver';
import { take } from "rxjs/operators";
import { Subject } from "rxjs";
import { channelNames } from "muse-js";
import { Line } from "react-chartjs-2";
import { zipSamples } from "muse-js";
import {
bandpassFilter,
epoch,
fft,
sliceFFT
} from "@neurosity/pipes";
import { chartStyles, generalOptions } from "../chartOptions";
import * as generalTranslations from "../translations/en";
import * as specificTranslations from "./translations/en";
import P5Wrapper from 'react-p5-wrapper';
import sketchFixation from "./sketchFixation"
export function getSettings() {
return {
cutOffLow: 2,
cutOffHigh: 20,
nbChannels: 4,
interval: 100,
bins: 256,
sliceFFTLow: 1,
sliceFFTHigh: 30,
duration: 1024,
srate: 256,
name: 'Alpha'
}
};
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: Settings.nbChannels }),
epoch({
duration: Settings.duration,
interval: Settings.interval,
samplingRate: Settings.srate
}),
fft({ bins: Settings.bins }),
sliceFFT([Settings.sliceFFTLow, Settings.sliceFFTHigh]),
catchError(err => {
console.log(err);
})
);
window.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) => {
if (index < 4) {
channel.datasets[0].data = data.psd[index];
channel.xLabels = data.freqs;
}
});
return {
ch0: alphaData.ch0,
ch1: alphaData.ch1,
ch2: alphaData.ch2,
ch3: alphaData.ch3
};
});
});
window.multicastAlpha$.connect();
console.log("Subscribed to " + Settings.name);
}
}
export function renderModule(channels) {
function renderCharts() {
return Object.values(channels.data).map((channel, index) => {
const options = {
...generalOptions,
scales: {
xAxes: [
{
scaleLabel: {
...generalOptions.scales.xAxes[0].scaleLabel,
labelString: specificTranslations.xlabel
}
}
],
yAxes: [
{
scaleLabel: {
...generalOptions.scales.yAxes[0].scaleLabel,
labelString: specificTranslations.ylabel
},
ticks: {
max: 25,
min: 0
}
}
]
},
elements: {
point: {
radius: 3
}
},
title: {
...generalOptions.title,
text: generalTranslations.channel + channelNames[index]
}
};
if (index === 0) {
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>
<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) {
return(
<Card title={'Record ' + Settings.name +' Data'} sectioned>
<Stack>
<ButtonGroup>
<Button
onClick={() => {
saveToCSV(Settings, "Closed");
recordPopChange();
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect}
>
{'Record Eyes Closed Data'}
</Button>
<Button
onClick={() => {
saveToCSV(Settings, "Open");
recordTwoPopChange();
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect}
>
{'Record Eyes Open Data'}
</Button>
</ButtonGroup>
<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) {
const numSamplesToSave = 50;
console.log('Saving ' + numSamplesToSave + ' samples...');
var localObservable$ = null;
const dataToSave = [];
console.log('making ' + Settings.name + ' headers')
// take one sample from selected observable object for headers
localObservable$ = window.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"
);
}
});
// put selected observable object into local and start taking samples
localObservable$ = window.multicastAlpha$.pipe(
take(numSamplesToSave)
);
// now with header in place subscribe to each epoch and log it
localObservable$.subscribe({
next(x) {
dataToSave.push(Date.now() + "," + Object.values(x).join(",") + "\n");
// logging is useful for debugging -yup
// console.log(x);
},
error(err) { console.log(err); },
complete() {
console.log('Trying to save')
var blob = new Blob(
dataToSave,
{type: "text/plain;charset=utf-8"}
);
saveAs(blob, Settings.name + "_" + condition + "_Recording_" + Date.now() + ".csv");
console.log('Completed');
}
});
}
@@ -0,0 +1,23 @@
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);
}
};
@@ -0,0 +1,12 @@
{
"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)"
}
@@ -24,6 +24,7 @@ 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';
@@ -35,19 +36,20 @@ export function getSettings () {
interval: 16,
bins: 256,
duration: 128,
srate: 256
srate: 256,
name: 'Animate'
}
};
export function buildPipe(Settings) {
if (window.subscriptionBands) window.subscriptionBands.unsubscribe();
if (window.subscriptionAnimate) window.subscriptionAnimate.unsubscribe();
window.pipeBands$ = null;
window.multicastBands$ = null;
window.subscriptionBands = null;
window.pipeAnimate$ = null;
window.multicastAnimate$ = null;
window.subscriptionAnimate = null;
// Build Pipe
window.pipeBands$ = zipSamples(window.source.eegReadings$).pipe(
window.pipeAnimate$ = zipSamples(window.source.eegReadings$).pipe(
bandpassFilter({
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
nbChannels: Settings.nbChannels }),
@@ -62,7 +64,7 @@ export function buildPipe(Settings) {
console.log(err);
})
);
window.multicastBands$ = window.pipeBands$.pipe(
window.multicastAnimate$ = window.pipeAnimate$.pipe(
multicast(() => new Subject())
);
}
@@ -70,10 +72,10 @@ export function buildPipe(Settings) {
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) => {
if (window.multicastAnimate$) {
window.subscriptionAnimate = window.multicastAnimate$.subscribe(data => {
setData(animateData => {
Object.values(animateData).forEach((channel, index) => {
if (index < 4) {
channel.datasets[0].data = [
data.delta[index],
@@ -87,15 +89,15 @@ export function setup(setData, Settings) {
});
return {
ch0: bandsData.ch0,
ch1: bandsData.ch1,
ch2: bandsData.ch2,
ch3: bandsData.ch3
ch0: animateData.ch0,
ch1: animateData.ch1,
ch2: animateData.ch2,
ch3: animateData.ch3
};
});
});
window.multicastBands$.connect();
window.multicastAnimate$.connect();
console.log("Subscribed to " + Settings.name);
}
}
@@ -108,13 +110,15 @@ export function renderModule(channels) {
const cube = 'cube';
const flock = 'flock';
const draw = 'draw';
const flock3d = 'flock3d';
const chartTypes = [
{ label: bands, value: bands },
{ label: tone, value: tone },
{ label: cube, value: cube },
{ label: flock, value: flock },
{ label: draw, value: draw}
{ label: draw, value: draw },
{ label: flock3d, value: flock3d }
];
// for picking a new animation
@@ -153,6 +157,9 @@ export function renderModule(channels) {
case draw:
thisSketch = sketchDraw;
break
case flock3d:
thisSketch = sketchFlock3D;
break
default: console.log("Error on switch to " + selectedAnimation)
}
@@ -232,7 +239,7 @@ export function renderSliders(setData, setSettings, status, Settings) {
}
return (
<React.Fragment>
<Card title={Settings.name + ' Settings'} sectioned>
<RangeSlider
disabled={status === generalTranslations.connect}
min={128} step={128} max={4096}
@@ -261,7 +268,7 @@ export function renderSliders(setData, setSettings, status, Settings) {
value={Settings.cutOffHigh}
onChange={handleCutoffHighRangeSliderChange}
/>
</React.Fragment>
</Card>
)
}
@@ -6,11 +6,11 @@ export default function sketchBands (p) {
let gamma = 0;
p.setup = function () {
p.createCanvas(p.windowWidth*.6, 800, p.WEBGL);
p.createCanvas(p.windowWidth*.5, p.windowWidth*.5, p.WEBGL);
};
p.windowResized = function() {
p.resizeCanvas(p.windowWidth*.6, 800);
p.resizeCanvas(p.windowWidth*.5, p.windowWidth*.5);
}
p.myCustomRedrawAccordingToNewPropsHandler = function (props) {
@@ -22,6 +22,9 @@ export default function sketchBands (p) {
};
p.draw = function () {
let unit = p.width/5;
p.background(256);
p.ambientMaterial(250);
p.noStroke();
@@ -34,38 +37,43 @@ export default function sketchBands (p) {
p.push();
p.translate(-100,0);
p.fill(255,0,0);
p.translate(-unit,0);
p.rotateY(50);
p.rotateX(50);
p.box(50, delta* 10, 100);
p.box(unit/2, delta* 10, unit);
p.pop();
p.push();
p.translate(-50,0);
p.fill(0,255,0);
p.translate(-unit/2,0);
p.rotateY(50);
p.rotateX(50);
p.box(50, theta* 10, 100);
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(50, alpha* 10, 100);
p.box(unit/2, alpha* 10, unit);
p.pop();
p.push();
p.translate(50,0);
p.fill(0,128, 128);
p.translate(unit/2,0);
p.rotateY(50);
p.rotateX(50);
p.box(50, beta* 10, 100);
p.box(unit/2, beta* 10, unit);
p.pop();
p.push();
p.translate(100,0);
p.fill(128,0,128);
p.translate(unit,0);
p.rotateY(50);
p.rotateX(50);
p.box(50, gamma* 10, 100);
p.box(unit/2, gamma* 10, unit);
p.pop();
};
@@ -8,6 +8,7 @@ export default function sketchDraw (p) {
let xVar = 0;
let yVar = 0;
let brushWidth = 50;
p.setup = function () {
p.createCanvas(p.windowWidth*.6, 500);
@@ -18,24 +19,24 @@ export default function sketchDraw (p) {
}
p.myCustomRedrawAccordingToNewPropsHandler = function (props) {
delta = Math.floor(50*props.delta);
theta = Math.floor(100*props.theta);
alpha = Math.floor(100*props.alpha);
beta = Math.floor(300*props.beta);
gamma = Math.floor(1000*props.gamma);
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 = beta;
yVar = alpha;
xVar = alpha;
yVar = beta;
if (xVar > p.width) {
xVar = p.width;
xVar = p.width-brushWidth/2;
}
if (yVar > p.height) {
yVar = p.height;
yVar = p.height-brushWidth/2;
}
console.log(xVar)
console.log(yVar)
// console.log(xVar)
// console.log(yVar)
};
@@ -46,7 +47,7 @@ export default function sketchDraw (p) {
p.draw = function () {
p.fill(theta, delta, gamma, 20);
p.noStroke();
p.ellipse(xVar, yVar, 50);
p.ellipse(xVar, yVar, brushWidth);
}
};
@@ -2,11 +2,10 @@ import p5 from "p5";
import "p5/lib/addons/p5.sound";
export default function sketchFlock (p) {
let delta = 0;
let theta = 0;
let alpha = 0;
let beta = 0;
let gamma = 0;
let xVar = 0;
let yVar = 0;
@@ -17,8 +16,8 @@ export default function sketchFlock (p) {
p.createCanvas(p.windowWidth*.6, 500);
flock = new p.Flock();
for (var i = 0; i < 100; i++) {
var b = new p.Boid(p.width / 2, p.height / 2, 2000);
for (var i = 0; i < 150; i++) {
var b = new p.Boid(p.width / 2, p.height / 2);
flock.addBoid(b)
}
};
@@ -27,32 +26,31 @@ export default function sketchFlock (p) {
p.createCanvas(p.windowWidth*.6, 500);
}
p.myCustomRedrawAccordingToNewPropsHandler = function (props) {
delta = Math.floor(props.delta);
theta = Math.floor(100*props.theta);
alpha = Math.floor(100*props.alpha);
beta = Math.floor(300*props.beta);
gamma = Math.floor(props.gamma);
xVar = beta;
yVar = alpha;
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;
xVar = p.width-5;
}
if (yVar > p.height) {
yVar = p.height;
yVar = p.height-5;
}
console.log(xVar)
console.log(yVar)
// console.log(xVar)
// console.log(yVar)
};
p.draw = function () {
p.background(0);
p.background(255);
p.fill(255,0,0)
p.push();
p.translate(xVar, yVar);
p.ellipse(0,0,5,5);
p.ellipse(0,0,10,10);
p.pop();
flock.run();
}
@@ -80,8 +78,8 @@ export default function sketchFlock (p) {
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 = 3; // Maximum speed
this.maxforce = 0.05; // Maximum steer ing force
this.maxspeed = 5; // Maximum speed
this.maxforce = 0.1; // Maximum steer ing force
}
p.Boid.prototype.run = function(boids) {
@@ -134,7 +132,7 @@ export default function sketchFlock (p) {
p.Boid.prototype.render = function() {
// Draw a triangle rotated in the direction of velocity
var theta = this.velocity.heading() + p.radians(90);
p.fill(255);
p.fill(0);
p.noStroke();
p.push();
p.translate(this.position.x, this.position.y);
@@ -231,7 +229,7 @@ export default function sketchFlock (p) {
};
p.Boid.prototype.mouuse = function(boids) {
var neighbordist = 100;
var neighbordist = 500;
var m = p.createVector(xVar, yVar);
var d = p5.Vector.dist(this.position, m);
if ((d > 0) && (d < neighbordist)) {
@@ -0,0 +1,540 @@
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;
}
}
}
@@ -37,7 +37,7 @@ export default function sketchTone (p) {
};
p.windowResized = function() {
p.createCanvas(p.windowWidth*.6, 800, p.WEBGL);
p.resizeCanvas(p.windowWidth*.6, 200);
}
p.draw = function () {
@@ -1,7 +1,9 @@
import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { Card, Stack, TextContainer, RangeSlider } from "@shopify/polaris";
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
import { saveAs } from 'file-saver';
import { take } from "rxjs/operators";
import { Subject } from "rxjs";
import { channelNames } from "muse-js";
@@ -30,7 +32,8 @@ export function getSettings () {
interval: 100,
bins: 256,
duration: 1024,
srate: 256
srate: 256,
name: 'Bands'
}
};
@@ -180,7 +183,7 @@ export function renderSliders(setData, setSettings, status, Settings) {
}
return (
<React.Fragment>
<Card title={Settings.name + ' Settings'} sectioned>
<RangeSlider
disabled={status === generalTranslations.connect}
min={128} step={128} max={4096}
@@ -209,7 +212,85 @@ export function renderSliders(setData, setSettings, status, Settings) {
value={Settings.cutOffHigh}
onChange={handleCutoffHighRangeSliderChange}
/>
</React.Fragment>
</Card>
)
}
export function renderRecord(recordPopChange, recordPop, status, Settings) {
return (
<Card title={'Record Data'} sectioned>
<Stack>
<ButtonGroup>
<Button
onClick={() => {
saveToCSV(Settings);
recordPopChange();
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect}
>
{'Save to CSV'}
</Button>
</ButtonGroup>
<Modal
open={recordPop}
onClose={recordPopChange}
title="Recording Data"
>
<Modal.Section>
<TextContainer>
<p>
Your data is currently recording,
once complete it will be downloaded as a .csv file
and can be opened with your favorite spreadsheet program.
Close this window once the download completes.
</p>
</TextContainer>
</Modal.Section>
</Modal>
</Stack>
</Card>
)
}
function saveToCSV(Settings) {
const numSamplesToSave = 50;
console.log('Saving ' + numSamplesToSave + ' samples...');
var localObservable$ = null;
const dataToSave = [];
console.log('making ' + Settings.name + ' headers')
dataToSave.push(
"Timestamp (ms),",
"delta0,delta1,delta2,delta3,deltaAux,",
"theta0,theta1,theta2,theta3,thetaAux,",
"alpha0,alpha1,alpha2,alpha3,alphaAux,",
"beta0,beta1,beta2,beta3,betaAux,",
"delta0,delta1,delta2,delta3,deltaAux\n"
);
// put selected observable object into local and start taking samples
localObservable$ = window.multicastBands$.pipe(
take(numSamplesToSave)
);
// now with header in place subscribe to each epoch and log it
localObservable$.subscribe({
next(x) {
dataToSave.push(Date.now() + "," + Object.values(x).join(",") + "\n");
// logging is useful for debugging -yup
// console.log(x);
},
error(err) { console.log(err); },
complete() {
console.log('Trying to save')
var blob = new Blob(
dataToSave,
{type: "text/plain;charset=utf-8"}
);
saveAs(blob, Settings.name + "_Recording_" + Date.now() + ".csv");
console.log('Completed');
}
});
}
@@ -0,0 +1,300 @@
import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { Subject } from "rxjs";
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
import { saveAs } from 'file-saver';
import { take } from "rxjs/operators";
import { 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,
nbChannels: 4,
interval: 1,
srate: 256,
duration: 1,
name: 'Evoked'
}
};
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: Settings.nbChannels }),
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) => {
if (index < 4) {
channel.datasets[0].data = data.data[index];
channel.xLabels = generateXTics(Settings.srate, Settings.duration);
channel.datasets[0].qual = standardDeviation(data.data[index])
}
});
return {
ch0: evokedData.ch0,
ch1: evokedData.ch1,
ch2: evokedData.ch2,
ch3: evokedData.ch3
};
});
});
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) {
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>
<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) {
const numSamplesToSave = 10000;
console.log('Saving ' + numSamplesToSave + ' samples...');
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"
);
}
});
// put selected observable object into local and start taking samples
localObservable$ = window.multicastEvoked$.pipe(
take(numSamplesToSave)
);
// 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');
}
});
}
Arquivo binário não exibido.

Depois

Largura:  |  Altura:  |  Tamanho: 224 KiB

@@ -0,0 +1,64 @@
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;
p.setup = function () {
p.createCanvas(300, 300);
p.frameRate(30);
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;
// var num = int(random(0, 11));
if (x % 20 === 0) { // When a target shown (every ith frame for now)
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);
}
};
@@ -0,0 +1,12 @@
{
"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)"
}
@@ -0,0 +1,161 @@
import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { Subject } from "rxjs";
import { TextContainer, Card, Stack } from "@shopify/polaris";
import { channelNames } from "muse-js";
import { Line } from "react-chartjs-2";
import { zipSamples } from "muse-js";
import {
bandpassFilter,
epoch
} from "@neurosity/pipes";
import { chartStyles, generalOptions } from "../chartOptions";
import * as generalTranslations from "../translations/en";
import * as specificTranslations from "./translations/en";
import { generateXTics, standardDeviation } from "../../utils/chartUtils";
export function getSettings () {
return {
cutOffLow: 2,
cutOffHigh: 20,
nbChannels: 4,
interval: 50,
srate: 256,
duration: 1024,
name: '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: Settings.nbChannels }),
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) => {
if (index < 4) {
channel.datasets[0].data = data.data[index];
channel.xLabels = generateXTics(Settings.srate, Settings.duration);
channel.datasets[0].qual = standardDeviation(data.data[index])
}
});
return {
ch0: heartRawData.ch0,
ch1: heartRawData.ch1,
ch2: heartRawData.ch2,
ch3: heartRawData.ch3
};
});
});
window.multicastHeartRaw$.connect();
console.log("Subscribed to HeartRaw");
}
}
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 + ', 128, 128)',
fill: false
},
point: {
radius: 0
}
},
animation: {
duration: 0
},
title: {
...generalOptions.title,
text: generalTranslations.channel + channelNames[index] + ' --- SD: ' + channel.datasets[0].qual
}
};
if (index === 1) {
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>
<div style={chartStyles.wrapperStyle.style}>{renderCharts()}</div>
</Card.Section>
</Card>
);
}
@@ -0,0 +1,14 @@
{
"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 (msec)",
"ylabel": "Voltage (\u03BCV)"
}
@@ -0,0 +1,334 @@
import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
import { saveAs } from 'file-saver';
import { take } from "rxjs/operators";
import { Subject } from "rxjs";
import { channelNames } from "muse-js";
import { Line } from "react-chartjs-2";
import { zipSamples } from "muse-js";
import {
bandpassFilter,
epoch,
fft,
sliceFFT
} from "@neurosity/pipes";
import { chartStyles, generalOptions } from "../chartOptions";
import * as generalTranslations from "../translations/en";
import * as specificTranslations from "./translations/en";
export function getSettings() {
return {
cutOffLow: .01,
cutOffHigh: 20,
nbChannels: 4,
interval: 100,
bins: 8192,
sliceFFTLow: 0.6,
sliceFFTHigh: 2,
duration: 4096,
srate: 256,
name: 'HeartSpectra'
}
};
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: Settings.nbChannels }),
epoch({
duration: Settings.duration,
interval: Settings.interval,
samplingRate: Settings.srate
}),
fft({ bins: Settings.bins }),
sliceFFT([Settings.sliceFFTLow, Settings.sliceFFTHigh]),
catchError(err => {
console.log(err);
})
);
window.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});
channel.peakF = channel.xLabels[indexOfMax(data.psd[1])];
});
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: 3
}
},
title: {
...generalOptions.title,
text: generalTranslations.channel +
channelNames[index] +
" - Estimated HR: " +
channel.peakF + " BPM"
}
};
return (
<Card.Section key={"Card_" + index}>
<Line key={"Line_" + index} data={channel} options={options} />
</Card.Section>
);
});
}
return (
<Card title={specificTranslations.title}>
<Card.Section>
<Stack>
<TextContainer>
<p>{specificTranslations.description}</p>
</TextContainer>
</Stack>
</Card.Section>
<Card.Section>
<div style={chartStyles.wrapperStyle.style}>{renderCharts()}</div>
</Card.Section>
</Card>
);
}
export function renderSliders(setData, setSettings, status, Settings) {
function resetPipeSetup(value) {
buildPipe(Settings);
setup(setData, Settings)
}
function handleIntervalRangeSliderChange(value) {
setSettings(prevState => ({...prevState, interval: value}));
resetPipeSetup();
}
function handleCutoffLowRangeSliderChange(value) {
setSettings(prevState => ({...prevState, cutOffLow: value}));
resetPipeSetup();
}
function handleCutoffHighRangeSliderChange(value) {
setSettings(prevState => ({...prevState, cutOffHigh: value}));
resetPipeSetup();
}
function handleSliceFFTLowRangeSliderChange(value) {
setSettings(prevState => ({...prevState, sliceFFTLow: value}));
resetPipeSetup();
}
function handleSliceFFTHighRangeSliderChange(value) {
setSettings(prevState => ({...prevState, sliceFFTHigh: value}));
resetPipeSetup();
}
function handleDurationRangeSliderChange(value) {
setSettings(prevState => ({...prevState, duration: value}));
resetPipeSetup();
}
return (
<Card title={Settings.name + ' Settings'} sectioned>
<RangeSlider
disabled={status === generalTranslations.connect}
min={128} step={128} max={4096}
label={'Epoch duration (Sampling Points): ' + Settings.duration}
value={Settings.duration}
onChange={handleDurationRangeSliderChange}
/>
<RangeSlider
disabled={status === generalTranslations.connect}
min={10} step={5} max={Settings.duration}
label={'Sampling points between epochs onsets: ' + Settings.interval}
value={Settings.interval}
onChange={handleIntervalRangeSliderChange}
/>
<RangeSlider
disabled={status === generalTranslations.connect}
min={.01} step={.5} max={Settings.cutOffHigh - .5}
label={'Cutoff Frequency Low: ' + Settings.cutOffLow + ' Hz'}
value={Settings.cutOffLow}
onChange={handleCutoffLowRangeSliderChange}
/>
<RangeSlider
disabled={status === generalTranslations.connect}
min={Settings.cutOffLow + .5} step={.5} max={Settings.srate/2}
label={'Cutoff Frequency High: ' + Settings.cutOffHigh + ' Hz'}
value={Settings.cutOffHigh}
onChange={handleCutoffHighRangeSliderChange}
/>
<RangeSlider
disabled={status === generalTranslations.connect}
min={1} max={Settings.sliceFFTHigh - 1}
label={'Slice FFT Lower limit: ' + Settings.sliceFFTLow + ' Hz'}
value={Settings.sliceFFTLow}
onChange={handleSliceFFTLowRangeSliderChange}
/>
<RangeSlider
disabled={status === generalTranslations.connect}
min={Settings.sliceFFTLow + 1}
label={'Slice FFT Upper limit: ' + Settings.sliceFFTHigh + ' Hz'}
value={Settings.sliceFFTHigh}
onChange={handleSliceFFTHighRangeSliderChange}
/>
</Card>
)
}
export function renderRecord(recordPopChange, recordPop, status, Settings) {
return(
<Card title={'Record ' + Settings.name +' Data'} sectioned>
<Stack>
<ButtonGroup>
<Button
onClick={() => {
saveToCSV(Settings);
recordPopChange();
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect}
>
{'Save to CSV'}
</Button>
</ButtonGroup>
<Modal
open={recordPop}
onClose={recordPopChange}
title="Recording Data"
>
<Modal.Section>
<TextContainer>
<p>
Your data is currently recording,
once complete it will be downloaded as a .csv file
and can be opened with your favorite spreadsheet program.
Close this window once the download completes.
</p>
</TextContainer>
</Modal.Section>
</Modal>
</Stack>
</Card>
)
}
function saveToCSV(Settings) {
const numSamplesToSave = 100;
console.log('Saving ' + numSamplesToSave + ' samples...');
var localObservable$ = null;
const dataToSave = [];
console.log('making ' + Settings.name + ' headers')
dataToSave.push(
"Timestamp (ms),",
"Estimated BPM",
"\n"
);
// put selected observable object into local and start taking samples
localObservable$ = window.multicastHeartSpectra$.pipe(
take(numSamplesToSave)
);
// 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;
}
@@ -0,0 +1,13 @@
{
"title": "Heart Rate (Beats per minute)",
"description": [
"Now we look at the same data, but in a different way. Instead of looking at the voltage over time, ",
"We now transform the data to show us what frequencies are present in the continuous signal. ",
"In this frequency domain, we now ignore time and consider how much power of each frequency there is in a segment of data. ",
"If you place your fingers the same way you did when looking at your ECG, you should start to see a peak ",
"Along the horizontal axis is the beats per minute, or the frequency of the peaks in your ECG. ",
"The vertical y-axis shows the power of the rhythms in the data at each frequency, or how large the changes are between peak and through of the oscillations. "
],
"xlabel": "Heart Frequency (BPM)",
"ylabel": "Power (\u03BCV\u00B2)"
}
@@ -170,6 +170,9 @@ export function renderModule(channels) {
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>
@@ -179,6 +182,9 @@ export function renderModule(channels) {
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>
@@ -196,6 +202,9 @@ export function renderModule(channels) {
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>
@@ -219,18 +228,23 @@ export function renderModule(channels) {
></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>
<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>
@@ -250,6 +264,9 @@ export function renderModule(channels) {
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 />
@@ -268,6 +285,9 @@ export function renderModule(channels) {
></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>
@@ -284,6 +304,10 @@ export function renderModule(channels) {
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()}
@@ -0,0 +1,233 @@
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 './sketchPredict';
import ml5 from 'ml5'
let knnClassifier = ml5.KNNClassifier();
export function getSettings() {
return {
cutOffLow: 2,
cutOffHigh: 20,
nbChannels: 4,
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: Settings.nbChannels }),
epoch({
duration: Settings.duration,
interval: Settings.interval,
samplingRate: Settings.srate
}),
fft({ bins: Settings.bins }),
sliceFFT([Settings.sliceFFTLow, Settings.sliceFFTHigh]),
catchError(err => {
console.log(err);
})
);
window.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) => {
if (index < 4) {
channel.datasets[0].data = data.psd[index];
channel.xLabels = data.freqs;
}
});
return {
ch0: predictData.ch0,
ch1: predictData.ch1,
ch2: predictData.ch2,
ch3: predictData.ch3
};
});
});
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>
</Card.Section>
<Card.Section>
<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};
window.thisLabel = 'A';
window.confidences = {A: 1, B: 0};
window.isPredicting = false;
window.enoughLabels = false;
export function renderRecord(status) {
const condA = "A";
const condB = "B";
// 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 === 2) {
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) {
window.thisLabel = result.label;
}
}
classify(); //recursive so it continues to run
}
//buttons for training at prediction
return(
<React.Fragment>
<Card title={'Record Training Data'} sectioned>
<Stack>
<ButtonGroup>
<Button
onClick={() => {
addExample('A');
}}
disabled={window.isPredicting || status === generalTranslations.connect}
>
{'Record ' + condA +' Data - Count: ' + window.exampleCounts['A']}
</Button>
<Button
onClick={() => {
addExample('B');
}}
disabled={window.isPredicting || status === generalTranslations.connect}
>
{'Record ' + condB + ' Data - Count: ' + window.exampleCounts['B']}
</Button>
</ButtonGroup>
</Stack>
</Card>
<Card title={'Predict current brain state after Training'} sectioned>
<Stack>
<ButtonGroup>
<Button
onClick={() => {
console.log('Attempting to classify state')
classify();
}}
disabled={window.isPredicting || !window.enoughLabels || status === generalTranslations.connect}
primary={true}
>
{'Predict State: ' + window.thisLabel + ', Confidence: ' + window.confidences[window.thisLabel].toFixed(2)}
</Button>
</ButtonGroup>
</Stack>
<Card.Section>
<P5Wrapper sketch={sketchPredict}
label={window.thisLabel}
confidences={window.confidences}
/>
</Card.Section>
</Card>
</React.Fragment>
)
}
@@ -0,0 +1,44 @@
export default function sketchPredict (p) {
let label;
let confidence;
p.setup = function () {
p.createCanvas(p.windowWidth*.6, 300);
};
p.windowResized = function() {
p.resizeCanvas(p.windowWidth*.6, 300);
}
p.myCustomRedrawAccordingToNewPropsHandler = function (props) {
label = props.label;
confidence = props.confidences[label];
};
p.draw = function () {
p.background(250, 250, 150);
p.fill(0);
p.strokeWeight(5);
p.line(p.width/2, 0, p.width/2, p.height);
p.textSize(30);
p.text('A', p.width/4, 30);
p.text('B', p.width-p.width/4, 30)
if (label === 'A') {
p.fill(120, 120, 250);
if (confidence > .8) {
p.ellipse(p.width/6, p.height/2, 60);
} else {
p.ellipse(p.width/3, p.height/2, 20);
}
} else {
p.fill(120, 250, 120);
if (confidence > .8) {
p.ellipse(p.width-p.width/6, p.height/2, 60);
} else {
p.ellipse(p.width-p.width/3, p.height/2, 20);
}
}
}
};
@@ -0,0 +1,13 @@
{
"title": "Predict brain states with a trained classifier",
"description": [
"In the next module we will train and test classifiers of brain data like we have been looking at so far. ",
"We will collect data in two different conditions, with the goal of inducing two different brain states. ",
"We will then train a classifier based on the pattern of activity over time, frequency, and space on the head. ",
"We will then use the classifier to predict on real time which of the two brain states are currently happening. ",
"That is, we will attempt to predict if peoples brain activity more closely resembles condition A or conditoin B ",
"Of the training data. "
],
"xlabel": "Frequency (Hz)",
"ylabel": "Power (\u03BCV\u00B2)"
}
@@ -2,7 +2,9 @@ import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { Subject } from "rxjs";
import { TextContainer, Card, Stack, RangeSlider } from "@shopify/polaris";
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
import { saveAs } from 'file-saver';
import { take } from "rxjs/operators";
import { channelNames } from "muse-js";
import { Line } from "react-chartjs-2";
@@ -28,7 +30,8 @@ export function getSettings () {
nbChannels: 4,
interval: 50,
srate: 256,
duration: 1024
duration: 1024,
name: 'Raw'
}
};
@@ -154,7 +157,6 @@ export function renderModule(channels) {
</Card>
);
}
export function renderSliders(setData, setSettings, status, Settings) {
@@ -184,7 +186,7 @@ export function renderSliders(setData, setSettings, status, Settings) {
}
return (
<React.Fragment>
<Card title={Settings.name + ' Settings'} sectioned>
<RangeSlider
disabled={status === generalTranslations.connect}
min={128} step={128} max={4096}
@@ -213,6 +215,107 @@ export function renderSliders(setData, setSettings, status, Settings) {
value={Settings.cutOffHigh}
onChange={handleCutoffHighRangeSliderChange}
/>
</React.Fragment>
</Card>
)
}
export function renderRecord(recordPopChange, recordPop, status, Settings) {
return (
<Card title={'Record ' + Settings.name + ' Data'} sectioned>
<Card.Section>
<p>
{"When you are recording raw data it is recommended you set the "}
{"number of sampling points between epochs onsets to be equal to the epoch duration. "}
{"This will ensure that consecutive rows of your output file are not overlapping in time."}
{"It will make the live plots appear more choppy."}
</p>
</Card.Section>
<Stack>
<ButtonGroup>
<Button
onClick={() => {
saveToCSV(Settings);
recordPopChange();
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect}
>
{'Save to CSV'}
</Button>
</ButtonGroup>
<Modal
open={recordPop}
onClose={recordPopChange}
title="Recording Data"
>
<Modal.Section>
<TextContainer>
<p>
Your data is currently recording,
once complete it will be downloaded as a .csv file
and can be opened with your favorite spreadsheet program.
Close this window once the download completes.
</p>
</TextContainer>
</Modal.Section>
</Modal>
</Stack>
</Card>
)
}
function saveToCSV(Settings) {
const numSamplesToSave = 50;
console.log('Saving ' + numSamplesToSave + ' samples...');
var localObservable$ = null;
const dataToSave = [];
console.log('making ' + Settings.name + ' headers')
// for each module subscribe to multicast and make header
// take one sample from selected observable object for headers
localObservable$ = window.multicastRaw$.pipe(
take(1)
);
//take one sample to get header info
localObservable$.subscribe({
next(x) {
dataToSave.push(
"Timestamp (ms),",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch0_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch1_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch2_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch3_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "chAux_" + f + "ms"}) + ",",
"info",
"\n"
);
}
});
// put selected observable object into local and start taking samples
localObservable$ = window.multicastRaw$.pipe(
take(numSamplesToSave)
);
// now with header in place subscribe to each epoch and log it
localObservable$.subscribe({
next(x) {
dataToSave.push(Date.now() + "," + Object.values(x).join(",") + "\n");
// logging is useful for debugging -yup
// console.log(x);
},
error(err) { console.log(err); },
complete() {
console.log('Trying to save')
var blob = new Blob(
dataToSave,
{type: "text/plain;charset=utf-8"}
);
saveAs(blob, Settings.name + "_Recording_" + Date.now() + ".csv");
console.log('Completed');
}
});
}
@@ -1,7 +1,9 @@
import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { Card, Stack, TextContainer, RangeSlider } from "@shopify/polaris";
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
import { saveAs } from 'file-saver';
import { take } from "rxjs/operators";
import { Subject } from "rxjs";
import { channelNames } from "muse-js";
@@ -31,7 +33,8 @@ export function getSettings() {
sliceFFTLow: 1,
sliceFFTHigh: 30,
duration: 1024,
srate: 256
srate: 256,
name: 'Spectra'
}
};
@@ -192,7 +195,7 @@ export function renderSliders(setData, setSettings, status, Settings) {
}
return (
<React.Fragment>
<Card title={Settings.name + ' Settings'} sectioned>
<RangeSlider
disabled={status === generalTranslations.connect}
min={128} step={128} max={4096}
@@ -235,9 +238,99 @@ export function renderSliders(setData, setSettings, status, Settings) {
value={Settings.sliceFFTHigh}
onChange={handleSliceFFTHighRangeSliderChange}
/>
</React.Fragment>
</Card>
)
}
export function renderRecord(recordPopChange, recordPop, status, Settings) {
return(
<Card title={'Record ' + Settings.name +' Data'} sectioned>
<Stack>
<ButtonGroup>
<Button
onClick={() => {
saveToCSV(Settings);
recordPopChange();
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect}
>
{'Save to CSV'}
</Button>
</ButtonGroup>
<Modal
open={recordPop}
onClose={recordPopChange}
title="Recording Data"
>
<Modal.Section>
<TextContainer>
<p>
Your data is currently recording,
once complete it will be downloaded as a .csv file
and can be opened with your favorite spreadsheet program.
Close this window once the download completes.
</p>
</TextContainer>
</Modal.Section>
</Modal>
</Stack>
</Card>
)
}
function saveToCSV(Settings) {
const numSamplesToSave = 50;
console.log('Saving ' + numSamplesToSave + ' samples...');
var localObservable$ = null;
const dataToSave = [];
console.log('making ' + Settings.name + ' headers')
// take one sample from selected observable object for headers
localObservable$ = window.multicastSpectra$.pipe(
take(1)
);
localObservable$.subscribe({
next(x) {
let freqs = Object.values(x.freqs);
dataToSave.push(
"Timestamp (ms),",
freqs.map(function(f) {return "ch0_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "ch1_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "ch2_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "ch3_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "chAux_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "f_" + f + "Hz"}) + "," ,
"info",
"\n"
);
}
});
// put selected observable object into local and start taking samples
localObservable$ = window.multicastSpectra$.pipe(
take(numSamplesToSave)
);
// now with header in place subscribe to each epoch and log it
localObservable$.subscribe({
next(x) {
dataToSave.push(Date.now() + "," + Object.values(x).join(",") + "\n");
// logging is useful for debugging -yup
// console.log(x);
},
error(err) { console.log(err); },
complete() {
console.log('Trying to save')
var blob = new Blob(
dataToSave,
{type: "text/plain;charset=utf-8"}
);
saveAs(blob, Settings.name + "_Recording_" + Date.now() + ".csv");
console.log('Completed');
}
});
}
@@ -0,0 +1,225 @@
import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { Card, Stack, TextContainer, RangeSlider} 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 sketchSpectro from './sketchSpectro'
import P5Wrapper from 'react-p5-wrapper';
export function getSettings () {
return {
cutOffLow: 1,
cutOffHigh: 100,
nbChannels: 4,
interval: 16,
bins: 128,
duration: 128,
srate: 256,
name: 'Spectro',
sliceFFTLow: 1,
sliceFFTHigh: 100,
}
};
export function buildPipe(Settings) {
if (window.subscriptionSpectro) window.subscriptionSpectro.unsubscribe();
window.pipeSpectro$ = null;
window.multicastSpectro$ = null;
window.subscriptionSpectro = null;
// Build Pipe
window.pipeSpectro$ = zipSamples(window.source.eegReadings$).pipe(
bandpassFilter({
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
nbChannels: Settings.nbChannels }),
epoch({
duration: Settings.duration,
interval: Settings.interval,
samplingRate: Settings.srate
}),
fft({ bins: Settings.bins }),
sliceFFT([Settings.sliceFFTLow, Settings.sliceFFTHigh]),
catchError(err => {
console.log(err);
})
);
window.multicastSpectro$ = window.pipeSpectro$.pipe(
multicast(() => new Subject())
);
}
export function setup(setData, Settings) {
console.log("Subscribing to " + Settings.name);
if (window.multicastSpectro$) {
window.subscriptionSpectro = window.multicastSpectro$.subscribe(data => {
setData(spectroData => {
Object.values(spectroData).forEach((channel, index) => {
if (index < 4) {
channel.datasets[0].data = data.psd[index];
channel.xLabels = data.freqs
}
});
return {
ch0: spectroData.ch0,
ch1: spectroData.ch1,
ch2: spectroData.ch2,
ch3: spectroData.ch3
};
});
});
window.multicastSpectro$.connect();
console.log("Subscribed to " + Settings.name);
}
}
export function renderModule(channels) {
function RenderCharts() {
return Object.values(channels.data).map((channel, index) => {
if (channel.datasets[0].data) {
window.psd = channel.datasets[0].data;
window.freqs = channel.xLabels;
if (channel.xLabels) {
window.bins = channel.xLabels.length;
}
}
//only left frontal channel
if (index === 1) {
return (
<React.Fragment key={'dum'}>
<Card.Section>
<P5Wrapper sketch={sketchSpectro}
psd={window.psd}
bins={window.bins}
/>
</Card.Section>
</React.Fragment>
);
} else {
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();
}
function handleSliceFFTHighRangeSliderChange(value) {
setSettings(prevState => ({...prevState, sliceFFTHigh: value}));
resetPipeSetup();
}
function handleSliceFFTLowRangeSliderChange(value) {
setSettings(prevState => ({...prevState, sliceFFTLow: 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>
)
}
@@ -0,0 +1,45 @@
import "p5/lib/addons/p5.sound";
export default function sketchSpectro (p) {
let spectrum;
let binCount;
let speed = 4;
// canvas is global so we can copy it
let cnv;
p.setup = function () {
cnv = p.createCanvas(p.windowWidth*.6, 400);
p.noStroke();
p.colorMode(p.RGB);
};
p.myCustomRedrawAccordingToNewPropsHandler = function (props) {
spectrum = props.psd;
binCount = props.bins;
};
p.windowResized = function() {
p.resizeCanvas(p.windowWidth*.6, 400);
}
p.draw = function () {
if (spectrum) {
// copy the sketch and move it over based on the speed
p.copy(cnv, 0, 0, p.width.toFixed(0), p.height.toFixed(0), -speed, 0, p.width.toFixed(0), p.height.toFixed(0));
// iterate thru current freq spectrum
for (let i = 0; i < binCount; i++) {
let value;
value = spectrum[i];
let c = (value/5)*255;
p.fill(255-c, 255-c, 255-c);
let percent = i / binCount;
let y = percent * p.height;
p.rect(p.width - speed, p.height - y, speed, p.height / binCount);
}
}
}
};
@@ -0,0 +1,12 @@
{
"title": "Spectrogram (Spectra over time)",
"description": [
"Back to the full spectra, we can also look at how this changes over time in a single image. ",
"This image, with time on the horizontal x-axis and freqency on the vertical y-axis is like a heat map or topographic map. ",
"Darker regions represent frequencies and times with larger power. ",
"Remember that on the spectra power is the vertical dimension, here is the is the colour."
],
"xlabel": "Time (ms)",
"ylabel": "Frequency (Hz)",
"zlabel": "Power (\u03BCV\u00B2)"
}
@@ -0,0 +1,383 @@
import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
import { saveAs } from 'file-saver';
import { take } from "rxjs/operators";
import { Subject } from "rxjs";
import { channelNames } from "muse-js";
import { Line } from "react-chartjs-2";
import { zipSamples } from "muse-js";
import {
bandpassFilter,
epoch,
fft,
sliceFFT
} from "@neurosity/pipes";
import { chartStyles, generalOptions } from "../chartOptions";
import * as generalTranslations from "../translations/en";
import * as specificTranslations from "./translations/en";
import P5Wrapper from 'react-p5-wrapper';
import sketchFlashSlow from './sketchFlashSlow';
import sketchFlashFast from './sketchFlashFast';
export function getSettings() {
return {
cutOffLow: 2,
cutOffHigh: 20,
nbChannels: 4,
interval: 100,
bins: 256,
sliceFFTLow: 1,
sliceFFTHigh: 30,
duration: 1024,
srate: 256,
name: 'Ssvep'
}
};
export function buildPipe(Settings) {
if (window.subscriptionSsvep) window.subscriptionSsvep.unsubscribe();
window.pipeSsvep$ = null;
window.multicastSsvep$ = null;
window.subscriptionSsvep = null;
// Build Pipe
window.pipeSsvep$ = zipSamples(window.source.eegReadings$).pipe(
bandpassFilter({
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
nbChannels: Settings.nbChannels }),
epoch({
duration: Settings.duration,
interval: Settings.interval,
samplingRate: Settings.srate
}),
fft({ bins: Settings.bins }),
sliceFFT([Settings.sliceFFTLow, Settings.sliceFFTHigh]),
catchError(err => {
console.log(err);
})
);
window.multicastSsvep$ = window.pipeSsvep$.pipe(
multicast(() => new Subject())
);
}
export function setup(setData, Settings) {
console.log("Subscribing to " + Settings.name);
if (window.multicastSsvep$) {
window.subscriptionSsvep = window.multicastSsvep$.subscribe(data => {
setData(ssvepData => {
Object.values(ssvepData).forEach((channel, index) => {
if (index < 4) {
channel.datasets[0].data = data.psd[index];
channel.xLabels = data.freqs;
}
});
return {
ch0: ssvepData.ch0,
ch1: ssvepData.ch1,
ch2: ssvepData.ch2,
ch3: ssvepData.ch3
};
});
});
window.multicastSsvep$.connect();
console.log("Subscribed to " + Settings.name);
}
}
export function renderModule(channels) {
function renderCharts() {
return Object.values(channels.data).map((channel, index) => {
const options = {
...generalOptions,
scales: {
xAxes: [
{
scaleLabel: {
...generalOptions.scales.xAxes[0].scaleLabel,
labelString: specificTranslations.xlabel
}
}
],
yAxes: [
{
scaleLabel: {
...generalOptions.scales.yAxes[0].scaleLabel,
labelString: specificTranslations.ylabel
},
ticks: {
max: 25,
min: 0
}
}
]
},
elements: {
point: {
radius: 3
}
},
title: {
...generalOptions.title,
text: generalTranslations.channel + channelNames[index]
}
};
if (index === 0) {
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>
<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) {
const cond1 = "11Hz";
const cond2 = "14Hz";
return(
<Card title={'Record ' + Settings.name +' Data'} sectioned>
<Stack>
<ButtonGroup>
<Button
onClick={() => {
saveToCSV(Settings, cond1);
recordPopChange();
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect}
>
{'Record ' + cond1 +' Data'}
</Button>
<Button
onClick={() => {
saveToCSV(Settings, cond2);
recordTwoPopChange();
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect}
>
{'Record ' + cond2 + ' Data'}
</Button>
</ButtonGroup>
<Modal
open={recordPop}
onClose={recordPopChange}
title={"Recording " + cond1 + " Data"}
>
<Modal.Section>
<Card.Section>
<P5Wrapper sketch={sketchFlashSlow} />
</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 " + cond2 + " Data"}
>
<Modal.Section>
<Card.Section>
<P5Wrapper sketch={sketchFlashFast} />
</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) {
const numSamplesToSave = 50;
console.log('Saving ' + numSamplesToSave + ' samples...');
var localObservable$ = null;
const dataToSave = [];
console.log('making ' + Settings.name + ' headers')
// take one sample from selected observable object for headers
localObservable$ = window.multicastSsvep$.pipe(
take(1)
);
localObservable$.subscribe({
next(x) {
let freqs = Object.values(x.freqs);
dataToSave.push(
"Timestamp (ms),",
freqs.map(function(f) {return "ch0_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "ch1_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "ch2_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "ch3_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "chAux_" + f + "Hz"}) + ",",
freqs.map(function(f) {return "f_" + f + "Hz"}) + "," ,
"info",
"\n"
);
}
});
// put selected observable object into local and start taking samples
localObservable$ = window.multicastSsvep$.pipe(
take(numSamplesToSave)
);
// now with header in place subscribe to each epoch and log it
localObservable$.subscribe({
next(x) {
dataToSave.push(Date.now() + "," + Object.values(x).join(",") + "\n");
// logging is useful for debugging -yup
// console.log(x);
},
error(err) { console.log(err); },
complete() {
console.log('Trying to save')
var blob = new Blob(
dataToSave,
{type: "text/plain;charset=utf-8"}
);
saveAs(blob, Settings.name + "_" + condition + "_Recording_" + Date.now() + ".csv");
console.log('Completed');
}
});
}
@@ -0,0 +1,32 @@
export default function sketchFlash (p) {
let x = 0;
p.setup = function () {
p.createCanvas(300, 300);
p.frameRate(30);
};
p.windowResized = function() {
p.createCanvas(300, 300);
}
p.mousePressed = function () {
p.background(256);
}
p.draw = function () {
p.background(255);
x = x+1;
if (x % 5 === 0) {
p.fill(0, 0, 0);
} else {
p.fill(255, 255, 255);
}
p.noStroke();
p.ellipse(p.width/2, p.height/2, 300);
p.fill(255,0,0);
p.text("+", p.width/2, p.height/2);
}
};
@@ -0,0 +1,34 @@
export default function sketchFlash (p) {
let x = 0;
p.setup = function () {
p.createCanvas(300, 300);
p.frameRate(30);
};
p.windowResized = function() {
p.createCanvas(300, 300);
}
p.mousePressed = function () {
p.background(256);
}
p.draw = function () {
p.background(255);
x = x+1;
if (x % 11 === 0) {
p.fill(0, 0, 0);
} else {
p.fill(255, 255, 255);
}
p.noStroke();
p.ellipse(p.width/2, p.height/2, 300);
p.fill(255,0,0);
p.text("+", p.width/2, p.height/2);
}
};
@@ -0,0 +1,13 @@
{
"title": "Steady-State Visual Evoked Potential (SSVEP) Experiment",
"description": [
"In the next demo we run our second experiment, comparing the spectra in another two conditions. ",
"When we see or hear rhythms in our environemnts (music, flashing lights, etc.), our brain picks up on those rhythms. ",
"The brain starts to pick up on the rhythms and begins to oscillate at the same frequency. ",
"This is called a steady-state response, or sometimes entrainement, and depends on cognitive factors like attention as well. ",
"Here we will compare the spectra in two conditions of different rhythmic visual stimulation.",
"WARNING: The following experiment utilizes flashing stimuli"
],
"xlabel": "Frequency (Hz)",
"ylabel": "Power (\u03BCV\u00B2)"
}
@@ -23,6 +23,13 @@ export const emptyChannelData = {
}
};
export const emptySingleChannelData = {
ch1: {
datasets: [{}]
}
};
export const generalOptions = {
scales: {
xAxes: [
+12 -5
Ver Arquivo
@@ -1,10 +1,17 @@
{
"title": "Choose your Module",
"types": {
"intro": "Introduction",
"raw": "Raw and Filtered Data",
"spectra": "Frequency Spectra",
"bands": "Frequency Bands",
"animate": "Brain Controlled Animation"
"intro": "1. Introduction",
"heartRaw": "2. Electrocardiogram (Heart beats)",
"heartSpectra": "3. Heart Rate (Beats per minute)",
"raw": "4. Raw and Filtered Data",
"spectra": "5. Frequency Spectra",
"bands": "6. Frequency Bands",
"animate": "7. Brain Controlled Animation",
"spectro": "8. Spectrogram (spectra over time)",
"alpha": "9. Eyes open vs. Eyes closed Experiment",
"ssvep": "10. Steady-State Visual Evoked Potential (SSVEP) Experiment",
"evoked": "11. Stimulus Evoked Event-related potential (ERP)",
"predict": "12. Predict brain states with a trained classifier"
}
}
-2
Ver Arquivo
@@ -1,7 +1,5 @@
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./components/App/App";
import registerServiceWorker from "./workers/registerServiceWorker";
ReactDOM.render(<App />, document.getElementById("root"));
registerServiceWorker();
-117
Ver Arquivo
@@ -1,117 +0,0 @@
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://goo.gl/SC7cgQ'
);
});
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
+952 -18
Ver Arquivo
Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff