Skip to content

Commit

Permalink
Added audio probes
Browse files Browse the repository at this point in the history
  • Loading branch information
sjawhar committed Sep 16, 2020
1 parent 32642a3 commit 82b3827
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 16 deletions.
5 changes: 4 additions & 1 deletion docs/Usage/Usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ By default, a Bluetooth search is conducted to find your Muse, and the first fou
**`-f, --filename STRING`**
Filename prefix for recorded data. If you supply a value like `filename.csv`, chunks will be saved in files called `filename.n.SOURCE.csv`, where "n" is the chunk number and "SOURCE" is the one of "ACC", "EEG", "GYRO", or "PPG". By default, "filename" is the datetime of the recording in YYYY-mm-dd_HH-MM-SS format.

**`-p, --probes MEAN [STD]`**
Sample user focus with audio probes. Provide one number X to sample every X minutes. Provide two numbers MEAN, STD to sample every Gaussian(MEAN, STD) minutes. Default is not to use probes.

**`-s, --skip-visualize`**
By default, a stability check is conducted after a connection is established. The streaming data is displayed and recording does not start until the signal stabilizes.

Expand All @@ -45,7 +48,7 @@ Record accelerometer measurements
**`-g, --gyro`**
Record gyroscope measurements

**`-p, --ppg`**
**`--ppg`**
Record PPG measurements

**`--no-eeg`**
Expand Down
Binary file added no_wander/assets/chime.wav
Binary file not shown.
13 changes: 11 additions & 2 deletions no_wander/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,22 @@ def record_setup_parser(parser):
help="Record gyroscope measurements",
)
parser.add_argument(
"-p",
"--ppg",
dest="sources",
action="append_const",
const=SOURCE_PPG,
help="Record PPG measurements",
)
parser.add_argument(
"-p",
"--probes",
help="Intermittently sample user focus with audio probes."
" Provide one number X to sample every X minutes."
" Provide two numbers MEAN, STD to sample every Gaussian(MEAN, STD) minutes.",
metavar=("MEAN", "STD"),
nargs="+",
type=float,
)
parser.add_argument(
"-s",
"--skip-visualize",
Expand Down Expand Up @@ -110,7 +119,7 @@ def record_run(args):
if not args.skip_visualize:
visualize()

run_session(get_duration(args.DURATION), args.sources, args.filename)
run_session(get_duration(args.DURATION), args.sources, args.filename, probes=args.probes)
end_stream()


Expand Down
5 changes: 4 additions & 1 deletion no_wander/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
DATASET_TRAIN = "train"
DATASET_VAL = "val"

DIR_ASSETS = (Path(__file__).parent / "assets").resolve()
DIR_DATA_DEFAULT = Path.cwd() / "data"
DIR_EPOCHS = "epochs"
DIR_FAILED = "failed"
Expand All @@ -20,8 +21,10 @@
EVENT_STREAMING_ERROR = "EVENT_STREAMING_ERROR"
EVENT_STREAMING_RESTARTED = "EVENT_STREAMING_RESTARTED"

MARKER_RECOVER = 1
MARKER_PROBE = -2
MARKER_SYNC = -1
MARKER_USER_FOCUS = 2
MARKER_USER_RECOVER = 1

PREPROCESS_EXTRACT_EEG = "extract-eeg"
PREPROCESS_NONE = "none"
Expand Down
69 changes: 57 additions & 12 deletions no_wander/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,24 @@
from .record import record_signals
from .stream import start_stream
from .constants import (
DIR_ASSETS,
EVENT_RECORD_CHUNK_START,
EVENT_SESSION_END,
EVENT_STREAMING_ERROR,
EVENT_STREAMING_RESTARTED,
MARKER_RECOVER,
MARKER_PROBE,
MARKER_SYNC,
MARKER_USER_FOCUS,
MARKER_USER_RECOVER,
)


KEYS_QUIT = ["esc", "q"]
KEYS_RECOVERY = ["up", "down", "left", "right", "space"]
SOUND_BELL = (Path(__file__).parent / "assets" / "bell.wav").resolve()
KEYS_RECOVERY = ["up", "right", "space"]
KEYS_FOCUS = ["left", "down"]
SOUND_PROBE = DIR_ASSETS / "chime.wav"
SOUND_SESSION_BEGIN = DIR_ASSETS / "bell.wav"
SOUND_SESSION_END = SOUND_SESSION_BEGIN

logger = logging.getLogger(__name__)

Expand All @@ -34,15 +40,33 @@ def get_duration(duration=None):
return duration * 60


def play_bell(wait=True):
def play_sound(sound_name, wait=True):
try:
bell = sound.Sound(SOUND_BELL)
bell.play()
sound_obj = sound.Sound(sound_name)
sound_obj.play()
if not wait:
return
core.wait(bell.getDuration())
core.wait(sound_obj.getDuration())
except Exception as error:
logger.error(f"Could not play {sound_name} sound: {error}")


def get_probe_intervals(duration, probes):
try:
# Default std of 0 if only mean is provided
mean, std, *_ = list(probes) + [0]
except:
logger.error("Could not play bell sound")
raise ValueError(f"Invalid probes configuration {probes}")

logger.debug(f"Generating probe intervals with mean {mean} and std {std}...")
intervals = []
while sum(intervals) < duration:
interval = np.random.normal(loc=mean, scale=std)
intervals.append(mean if interval <= 0 else interval)
intervals = intervals[:-1]

logger.debug(f"{len(intervals)} probe intervals generated!")
return intervals


def handle_signals_message(message, outlet):
Expand Down Expand Up @@ -70,7 +94,10 @@ def handle_keypress(keys, outlet):
for key, timestamp in keys:
logger.debug(f"{key} pressed at time {timestamp}")
if key in KEYS_RECOVERY:
outlet.push_sample([MARKER_RECOVER], timestamp)
outlet.push_sample([MARKER_USER_RECOVER], timestamp)
continue
elif key in KEYS_FOCUS:
outlet.push_sample([MARKER_USER_FOCUS], timestamp)
continue
elif key not in KEYS_QUIT:
continue
Expand All @@ -82,7 +109,7 @@ def handle_keypress(keys, outlet):
return quit


def run_session(duration, sources, filepath):
def run_session(duration, sources, filepath, probes=None):
info = StreamInfo(
name="Markers",
type="Markers",
Expand All @@ -99,7 +126,7 @@ def run_session(duration, sources, filepath):
)
text.draw()
session_window.flip()
play_bell(wait=False)
play_sound(SOUND_SESSION_BEGIN, wait=False)

clock = core.Clock()
start_time = time()
Expand All @@ -112,12 +139,23 @@ def run_session(duration, sources, filepath):
)
signals_process.start()

next_probe_time = None
if probes is not None:
logger.info(f"Audio probes enabled")
probe_intervals = get_probe_intervals(duration, np.array(probes) * 60)
probe_times = list(np.cumsum(probe_intervals) + time())[::-1]
next_probe_time = probe_times.pop()
logger.debug(
f"First of {1 + len(probe_times)} audio probes at {next_probe_time}"
)

while signals_process.is_alive():
try:
if signals_conn.poll():
message = handle_signals_message(signals_conn.recv(), outlet)
if message is not None:
signals_conn.send(message)

keys = event.getKeys(timeStamped=clock)
if keys is not None and handle_keypress(keys, outlet) is True:
logger.info("Triggering end of session")
Expand All @@ -126,10 +164,17 @@ def run_session(duration, sources, filepath):
signals_process.join()
logger.info("Session ended")
break

if next_probe_time is not None and time() > next_probe_time:
marker_outlet.push_sample([MARKER_PROBE], clock.getTime())
play_sound(SOUND_PROBE, wait=False)
next_probe_time = None if len(probe_times) == 0 else probe_times.pop()
logger.debug(f"Audio probe played. Next probe at {next_probe_time}")

except KeyboardInterrupt:
break

play_bell()
play_sound(SOUND_SESSION_END)
session_window.close()

if signals_process.is_alive():
Expand Down

0 comments on commit 82b3827

Please sign in to comment.