Skip to content

Commit

Permalink
stimuli: AudioStims returned from play_item should know their producer
Browse files Browse the repository at this point in the history
ref: #16

`Playlist.play_item` returns a new generator that has default
values for its producer. This means that some sequences of
manually played items in a playlist will not register as
coming from different producers - so events describing the
change in the playing stimulus item will not be emitted and
thus will be missing from the TOC file
  • Loading branch information
nzjrs committed Feb 11, 2022
1 parent 9412684 commit 650bb60
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 7 deletions.
6 changes: 4 additions & 2 deletions flyvr/audio/signal_producer.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ def new_silence(cls, data):
def chunk_producers_differ(prev: Optional[SampleChunk], this: Optional[SampleChunk]):
if (prev is not None) and (this is not None):
return this.mixed_producer or (prev.producer_identifier,
prev.producer_instance_n,
prev.producer_playlist_n) != (this.producer_identifier,
this.producer_instance_n,
this.producer_playlist_n)
elif (prev is None) and (this is not None):
return True
Expand Down Expand Up @@ -265,15 +267,15 @@ def __init__(self, stims, identifier=None, next_event_callback=None):

self._data = np.zeros((self.chunk_size, self.chunk_width), dtype=self.dtype)

def data_generator(self) -> Iterator[Optional[SampleChunk]]:
def data_generator(self, producer_instance_n_override=None) -> Iterator[Optional[SampleChunk]]:
"""
Create a data generator for this signal. Each signal passed to the constructor will be yielded as a separate
column of the data chunk returned by this generator.
"""

# Initialize data generators for these signals in the play list.
# Wrap each generator in a chunker with the same size.
data_gens = [chunker(s.data_generator(), self.chunk_size) for s in self._stims]
data_gens = [chunker(s.data_generator(producer_instance_n_override), self.chunk_size) for s in self._stims]

while True:

Expand Down
10 changes: 5 additions & 5 deletions flyvr/audio/stimuli.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,15 @@ def describe(self):
'max_value': self.__max_value,
'min_value': self.__min_value}

def data_generator(self) -> Iterator[Optional[SampleChunk]]:
def data_generator(self, producer_instance_n_override=None) -> Iterator[Optional[SampleChunk]]:
"""
Return a generator that yields the data member when next is called on it. Simply provides another interface to
the same data stored in the data member.
"""
while True:
self.num_samples_generated = self.num_samples_generated + self.data.shape[0]
chunk = SampleChunk(data=self.data, producer_identifier=self.identifier,
producer_instance_n=self.producer_instance_n)
producer_instance_n=producer_instance_n_override or self.producer_instance_n)
self.trigger_next_callback(chunk)
yield chunk

Expand Down Expand Up @@ -865,16 +865,16 @@ def from_playlist_definition(cls, stim_playlist, basedirs, paused_fallback, defa
attenuator=attenuator)

def play_item(self, identifier):
# it's actually debatable if it's best do it this way or explicitly reset a global+sticky next_id
for stim in self._stims:
if stim.identifier == identifier:
return stim.data_generator()
SignalProducer.instances_created += 1
return stim.data_generator(-100 - SignalProducer.instances_created)
raise ValueError('%s not found' % identifier)

def play_pause(self, pause):
self.paused = pause

def data_generator(self) -> Iterator[Optional[SampleChunk]]:
def data_generator(self, producer_instance_n_override=None) -> Iterator[Optional[SampleChunk]]:
"""
Return a generator that yields each AudioStim in the playlist in succession. If shuffle_playback is set to true
then we will get a non-repeating randomized sequence of all stimuli, then they will be shuffled, and the process
Expand Down
83 changes: 83 additions & 0 deletions tests/audio/test_samplechunks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest

import copy
import itertools

from flyvr.audio.stimuli import stimulus_factory, AudioStimPlaylist
from flyvr.audio.signal_producer import chunker, SampleChunk, chunk_producers_differ, SignalProducer
Expand Down Expand Up @@ -302,3 +303,85 @@ def test_stim_playlist_chunker_chunks_for_csv_explanation(monkeypatch, chunksize
pd.DataFrame(recs).to_csv(_path, index=False)

print(_path)

def test_play_item_produces_new_instance_chunk(stimplaylist):
# test new data generator instance
a = stimplaylist.play_item('sin10hz')
b = stimplaylist.play_item('constant1')
assert a != b
assert hash(a) != hash(b)
c = stimplaylist.play_item('sin10hz')
assert a != c
assert hash(a) != hash(c)

# next() on an individual AudioStim without a chunker will just return the entire
# array in a loop, aka from the same chunk producer
ca0 = next(a)
assert ca0.producer_identifier == 'sin10hz'
assert ca0.data.shape == (1200,)
ca1 = next(a)
assert ca1.data.shape == (1200,)
assert ca1.producer_identifier == 'sin10hz'
assert not chunk_producers_differ(ca0, ca1)
ca2 = next(a)
assert ca2.data.shape == (1200,)
assert ca2.producer_identifier == 'sin10hz'
assert not chunk_producers_differ(ca0, ca2)

# first chunk on different AudioStim from the previous play_item
cb0 = next(b)
assert cb0.data.shape == (1200,)
assert cb0.producer_identifier == 'constant1'
assert chunk_producers_differ(ca0, cb0)

# first chunk on same AudioStim from the first sin10hz
cc0 = next(c)
assert cc0.data.shape == (1200,)
assert cc0.producer_identifier == 'sin10hz'
assert chunk_producers_differ(ca0, cc0)


def test_play_item_produces_new_instance_chunk_chunker(stimplaylist):
a = chunker(stimplaylist.play_item('sin10hz'), 600)
b = chunker(stimplaylist.play_item('constant1'), 600)
c = chunker(stimplaylist.play_item('sin10hz'), 600)

ca0 = next(a)
assert ca0.producer_identifier == 'sin10hz'
assert ca0.data.shape == (600,)
ca1 = next(a)
assert ca1.producer_identifier == 'sin10hz'
assert ca1.data.shape == (600,)
assert not chunk_producers_differ(ca0, ca1)
# loops back round to the start
ca2 = next(a)
assert ca2.producer_identifier == 'sin10hz'
assert ca2.data.shape == (600,)
assert not chunk_producers_differ(ca1, ca2)
assert not chunk_producers_differ(ca0, ca2)

cb0 = next(b)
assert cb0.producer_identifier == 'constant1'
assert cb0.data.shape == (600,)

assert chunk_producers_differ(ca0, cb0)
assert chunk_producers_differ(ca2, cb0)

cc0 = next(c)
assert cc0.producer_identifier == 'sin10hz'
assert cc0.data.shape == (600,)
cc1 = next(c)
assert cc1.producer_identifier == 'sin10hz'
assert cc1.data.shape == (600,)
assert not chunk_producers_differ(cc0, cc1)
# loops back round to the start
cc2 = next(c)
assert cc2.producer_identifier == 'sin10hz'
assert cc2.data.shape == (600,)
assert not chunk_producers_differ(cc1, cc2)
assert not chunk_producers_differ(cc0, cc2)

# all chunks on same AudioStim differ
for a,c in itertools.product((ca0,ca1,ca2), (cc0,cc1,cc2)):
chunk_producers_differ(a, c)

0 comments on commit 650bb60

Please sign in to comment.