Skip to content

Commit

Permalink
enh: offset voice key, version 0.5
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremygray committed Sep 28, 2015
1 parent ad2fdb2 commit 71e2d15
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 336 deletions.
88 changes: 35 additions & 53 deletions psychopy/voicekey/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,16 @@
Copyright (c) Jeremy R. Gray, 2015
License: Distributed under the terms of the GPLv3
Version: 0.4
Dev status: beta. Can work well in some circumstances, not widely tested.
Smoother with 64-bit python and pyo.
_BaseVoiceKey is the main abstract class. Subclass and override the detect()
method. See SimpleThresholdVoiceKey
See readme.txt for notes, vk_plot.py for demos and testing.
method. See SimpleThresholdVoiceKey or OnsetVoiceKey for examples.
"""

from __future__ import division

__version__ = 0.5

import sys
import os
import numpy as np
Expand All @@ -32,8 +30,7 @@
# pyo_server will point to a booted pyo server once pyo_init() is called:
pyo_server = None

# Various bits and pieces:
from . signal import _BaseVoiceKeySignal
# helper functions for time, signal processing, and file I/O:
from . vk_tools import *

# Constants:
Expand Down Expand Up @@ -207,7 +204,7 @@ def _set_signaler(self):
def _set_tables(self):
"""Set up the pyo tables (allocate memory, etc).
One source -> three pyo tables: chunk=short, whole=all, baseline
One source -> three pyo tables: chunk=short, whole=all, baseline.
triggers fill tables from self._source; make triggers in .start()
"""
sec_per_chunk = self.msPerChunk / 1000.
Expand Down Expand Up @@ -439,9 +436,10 @@ def wait_for_event(self, plus=0):
if naptime > 0:
sleep(naptime)
self.stop()
# next sleep() helps avoid pyo "ReferenceError: weakly-referenced
# object no longer exists"
# next sleep() helps avoid pyo error:
# "ReferenceError: weakly-referenced object no longer exists"
sleep(1.5 * self.msPerChunk / 1000.)

return self.elapsed

def save(self, ftype='', dtype='int16'):
Expand Down Expand Up @@ -515,62 +513,46 @@ def detect(self):
self.trip()


class VoicelessPlosiveVoiceKey(_BaseVoiceKey):
"""Class to detect and signal the offset of a vowel followed by a voiceless
plosive, e.g. the end of "ah" in utterance "ah pa".
class OffsetVoiceKey(_BaseVoiceKey):
"""Class to detect the offset of a single-word utterance.
Ends the recording after a delay; default = 300ms later.
"""
def __init__(self, sec=1.5, msPerChunk=2, file_out='', file_in='',
duration=0.070, proportion=0.7, signaler=None,
start=0, stop=-1, baseline=0):
def __init__(self, sec=10, file_out='', file_in='', delay=0.3, **kwargs):
"""Adjust parameters `duration` and `proportion` as needed.
"""
config = {'sec': sec,
'msPerChunk': msPerChunk,
'file_out': file_out,
'file_in': file_in,
'duration': duration, # min duration of vowel, in sec
'proportion': proportion, # min prop of chunks > threshold
'signaler': signaler, # obj for obj.signal() upon event
'start': start,
'stop': stop,
'baseline': baseline,
'delay': delay,
}
super(VoicelessPlosiveVoiceKey, self).__init__(**config)
kwargs.update(config)
super(OffsetVoiceKey, self).__init__(**kwargs)

def detect(self):
"""Detect the near-end of the first sustained speech-like sound.
Called every chunk, so keep it efficient.
Define multiple conditions. Trip (= trigger the event) if all are met.
- minimum time has elapsed (baseline period)
- have gone above a minimum threshold recently
- met that threshold for some proportion of recent chunks (hold time)
- sound is currently greatly attenuated (trailing edge)
"""Wait for onset, offset, delay, then end the recording.
"""

if self.event_detected or not self.baseline:
return

thr_norm = 0.03 * self.max_bp # 3% of recent max value; not ensured to be recent
if not hasattr(self, '_hold'):
# compute once, cache
self._hold = -1 * int(self.config['duration'] * 1000. / self.msPerChunk)
self._offset = -1 * int(8. / self.msPerChunk) # ms -> chunks

vals = self.power_bp[self._hold:self._offset]
max_val = np.max(vals)
prop_over_thr = np.mean(vals > thr_norm) # mean of 0, 1's
loud_enough = max_val > 5 * self.baseline and max_val > 500

recent = np.mean(self.power_bp[self._offset:])
quiet_recently = recent < 2 * thr_norm

conditions = (prop_over_thr > self.config['proportion'],
loud_enough,
quiet_recently)
if all(conditions):
if not self.event_onset:
window = 5 # chunks
threshold = 10 * self.baseline
conditions = all([x > threshold for x in self.power_bp[-window:]])
if conditions:
self.event_lag = window * self.msPerChunk / 1000.
self.event_onset = self.elapsed - self.event_lag
self.event_offset = 0
elif not self.event_offset:
window = 25
threshold = 10 * self.baseline
conditions = all([x < threshold for x in self.power_bp[-window:]])
if conditions:
self.event_lag = window * self.msPerChunk / 1000.
self.event_offset = self.elapsed - self.event_lag
self.event_time = self.event_offset # for plotting
elif self.elapsed > self.event_offset + self.config['delay']:
self.trip()
self.stop()


### ----- Convenience classes -------------------------------------------------
Expand Down

0 comments on commit 71e2d15

Please sign in to comment.