From 650bb606d51d86ded7d28a6e324e288cf9cda17e Mon Sep 17 00:00:00 2001 From: John Stowers Date: Fri, 11 Feb 2022 13:03:45 +0100 Subject: [PATCH] stimuli: AudioStims returned from play_item should know their producer 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 --- flyvr/audio/signal_producer.py | 6 ++- flyvr/audio/stimuli.py | 10 ++-- tests/audio/test_samplechunks.py | 83 ++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 7 deletions(-) diff --git a/flyvr/audio/signal_producer.py b/flyvr/audio/signal_producer.py index 6298ac73..7263426a 100644 --- a/flyvr/audio/signal_producer.py +++ b/flyvr/audio/signal_producer.py @@ -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 @@ -265,7 +267,7 @@ 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. @@ -273,7 +275,7 @@ def data_generator(self) -> Iterator[Optional[SampleChunk]]: # 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: diff --git a/flyvr/audio/stimuli.py b/flyvr/audio/stimuli.py index cc0fd87a..70e6f12c 100644 --- a/flyvr/audio/stimuli.py +++ b/flyvr/audio/stimuli.py @@ -150,7 +150,7 @@ 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. @@ -158,7 +158,7 @@ def data_generator(self) -> Iterator[Optional[SampleChunk]]: 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 @@ -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 diff --git a/tests/audio/test_samplechunks.py b/tests/audio/test_samplechunks.py index 7becabbf..e49d2730 100644 --- a/tests/audio/test_samplechunks.py +++ b/tests/audio/test_samplechunks.py @@ -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 @@ -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) +