Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/player config #648

Merged
merged 45 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
f545e22
subclass Playback for player types
BeritJanssen Nov 6, 2023
2752321
rules: switch to subclassed playback
BeritJanssen Nov 6, 2023
5402163
toontjehoger changes: rename SpectogramPlayer, reorganize labels
BeritJanssen Nov 6, 2023
24efd86
correct __init__ logic
BeritJanssen Nov 6, 2023
409a278
enforce sections as first argument for all player types
BeritJanssen Nov 6, 2023
5b0bbf6
change internal naming of the player type
BeritJanssen Nov 6, 2023
696d59f
fix test
BeritJanssen Nov 6, 2023
86a1047
reorganize argument passing frontend
BeritJanssen Nov 6, 2023
efd4a9e
fix checking of play_method
BeritJanssen Nov 6, 2023
d1dc4c9
Merge branch 'develop' into feature/player-config
BeritJanssen Nov 14, 2023
d5c1cde
Merge branch 'develop' into feature/player-config
BeritJanssen Nov 28, 2023
179de48
fix merge bug
BeritJanssen Nov 28, 2023
c57de43
remove unused code
BeritJanssen Nov 28, 2023
948a4af
use mute to determine whether audio should be played
BeritJanssen Nov 28, 2023
01bd3c9
Merge branch 'develop' into feature/player-config
BeritJanssen Dec 4, 2023
dc9a165
SongSync: mute silent view
BeritJanssen Dec 4, 2023
094c052
Autoplay: reuse playSection
BeritJanssen Dec 4, 2023
5cde0c8
fix problems with playhead and component cleanup
BeritJanssen Dec 4, 2023
2807c55
remove play_method from methods
BeritJanssen Dec 5, 2023
0fd7c6b
update props passed to Playback children
BeritJanssen Dec 5, 2023
23b2f79
remove label_style argument; move show_animation argument
BeritJanssen Dec 5, 2023
5379ef5
remove unused dependencies MultiPlayer
BeritJanssen Dec 5, 2023
aea52b6
fix React warnings
BeritJanssen Dec 5, 2023
2b0f2df
player labels and test in backend
BeritJanssen Dec 11, 2023
acea019
bugfix: correct import of get_test_playback
BeritJanssen Dec 11, 2023
298a0d1
bugfix: pass through playMethod to Preload
BeritJanssen Dec 11, 2023
a1f19b9
Merge branch 'develop' into feature/player-config
BeritJanssen Dec 11, 2023
fe4acb8
Merge branch 'develop' into feature/player-config
BeritJanssen Dec 18, 2023
671032c
remove label functions which were moved to backend
BeritJanssen Dec 18, 2023
1f33c33
change Playback constructor for toontjehoger_3
BeritJanssen Dec 19, 2023
bc5556b
adjust props after develop merge
BeritJanssen Jan 8, 2024
68591f4
add test
BeritJanssen Jan 9, 2024
ae244a7
Merge branch 'develop' into feature/player-config
BeritJanssen Jan 9, 2024
131879c
Merge branch 'develop' into feature/player-config
BeritJanssen Jan 9, 2024
18f9374
bugfix: correct order of arguments to playAudio
BeritJanssen Jan 15, 2024
f840480
remove unused webAudio.playBuffer
BeritJanssen Jan 15, 2024
2e1ac0b
bugfix: correct playheadShift
BeritJanssen Jan 15, 2024
a346b23
remove obsolete imports
BeritJanssen Jan 15, 2024
df1580e
remove useRefs
BeritJanssen Jan 15, 2024
8ee1841
add mediaRoot to audio.load()
BeritJanssen Jan 15, 2024
929709e
fix: useState for running in Autoplay
BeritJanssen Jan 15, 2024
f218c1d
Merge branch 'develop' into feature/player-config
BeritJanssen Jan 15, 2024
87644a6
VisualMatchingPairs as Playback subclass
BeritJanssen Jan 15, 2024
21a8058
fix tests
BeritJanssen Jan 15, 2024
ed6ffb9
fix linting issues
BeritJanssen Jan 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 134 additions & 45 deletions backend/experiment/actions/playback.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,145 @@
from .base_action import BaseAction

# player types
TYPE_AUTOPLAY = 'AUTOPLAY'
TYPE_BUTTON = 'BUTTON'
TYPE_IMAGE = 'IMAGE'
TYPE_MULTIPLAYER = 'MULTIPLAYER'
TYPE_MATCHINGPAIRS = 'MATCHINGPAIRS'
TYPE_VISUALMATCHINGPAIRS = 'VISUALMATCHINGPAIRS'

# playback methods
PLAY_EXTERNAL = 'EXTERNAL'
PLAY_HTML = 'HTML'
PLAY_BUFFER = 'BUFFER'
PLAY_NOAUDIO = 'NOAUDIO'


class Playback(BaseAction):
''' A playback wrapper for different kinds of players
- player_type: can be one of the following:
- 'AUTOPLAY' - player starts automatically
- 'BUTTON' - display one play button
- 'MULTIPLAYER' - display multiple small play buttons, one per section
- 'SPECTROGRAM' - extends multiplayer with a list of spectrograms
- 'MATCHINGPAIRS': Use for the matching pairs game.
- 'VISUALMATCHINGPAIRS': Use for the visual matching pairs game.
''' A playback base class for different kinds of players
- sections: a list of sections (in many cases, will only contain *one* section)
- preload_message: text to display during preload
- instruction: text to display during presentation of the sound
- play_config: define to override the following values:
- play_method:
- 'BUFFER': Use webaudio buffers. (recommended for stimuli up to 45s)
- 'HTML': Use the HTML tag. (recommended for stimuli longer than 45s)
- 'EXTERNAL': Use for externally hosted audio files. Web-audio api will be disabled
- 'PREFETCH': Files will be fetched and cached in the browser.
- ready_time: time before presentation of sound
- timeout_after_playback: pause in ms after playback has finished
- playhead: from where the audio file should play (offset in seconds from start)
- mute: whether audio should be muted
- auto_play: whether sound will start automatically
- stop_audio_after: after how many seconds playback audio should be stopped
- show_animation: whether to show an animation during playback
- (multiplayer) label_style: player index number style: NUMERIC, ALPHABETIC, ROMAN or empty (no label)
- play_once: the sound can only be played once
- resume_play: if the playback should resume from where a previous view left off
'''

TYPE_AUTOPLAY = 'AUTOPLAY'
TYPE_BUTTON = 'BUTTON'
TYPE_MULTIPLAYER = 'MULTIPLAYER'
TYPE_SPECTROGRAM = 'SPECTROGRAM'

def __init__(self, sections, player_type='AUTOPLAY', preload_message='', instruction='', play_config=None):
- play_from: where in the audio file to start playing/
- ready_time: how long to show the "Preload" view (loading spinner)
- show_animation: whether to show animations with this player
- mute: whether to mute the audio
- timeout_after_playback: once playback has finished, add optional timeout (in seconds) before proceeding
- stop_audio_after: stop playback after so many seconds
- resume_play: if the playback should resume from where a previous view left off
'''

def __init__(self,
sections,
preload_message='',
instruction='',
play_from=0,
ready_time=0,
show_animation=False,
mute=False,
timeout_after_playback=None,
stop_audio_after=None,
resume_play=False):
self.sections = [{'id': s.id, 'url': s.absolute_url(), 'group': s.group}
for s in sections]
self.ID = player_type
if str(sections[0].filename).startswith('http'):
self.play_method = PLAY_EXTERNAL
elif sections[0].duration > 45:
self.play_method = PLAY_HTML
else:
self.play_method = PLAY_BUFFER
self.show_animation = show_animation
self.preload_message = preload_message
self.instruction = instruction
self.play_config = {
'play_method': 'BUFFER',
'external_audio': False,
'ready_time': 0,
'playhead': 0,
'show_animation': False,
'mute': False,
'play_once': False,
'resume_play': False
}
if play_config:
self.play_config.update(play_config)
self.play_from = play_from
self.mute = mute
self.ready_time = ready_time
self.timeout_after_playback = timeout_after_playback
self.stop_audio_after = stop_audio_after
self.resume_play = resume_play


class Autoplay(Playback):
'''
This player starts playing automatically
- show_animation: if True, show a countdown and moving histogram
'''

def __init__(self, sections, **kwargs):
super().__init__(sections, **kwargs)
self.ID = TYPE_AUTOPLAY


class PlayButton(Playback):
'''
This player shows a button, which triggers playback
- play_once: if True, button will be disabled after one play
'''

def __init__(self, sections, play_once=False, **kwargs):
super().__init__(sections, **kwargs)
self.ID = TYPE_BUTTON
self.play_once = play_once


class Multiplayer(PlayButton):
'''
This is a player with multiple play buttons
- stop_audio_after: after how many seconds to stop audio
- label_style: set if players should be labeled in alphabetic / numeric / roman style (based on player index)
- labels: pass list of strings if players should have custom labels
'''

def __init__(self, sections, stop_audio_after=5, labels=[], **kwargs):
super().__init__(sections, **kwargs)
self.ID = TYPE_MULTIPLAYER
self.stop_audio_after = stop_audio_after
if labels:
if len(labels) != len(self.sections):
raise UserWarning(
'Number of labels and sections for the play buttons do not match')
self.labels = labels


class ImagePlayer(PlayButton):
'''
This is a special case of the Multiplayer:
it shows an image next to each play button
'''

def __init__(self, sections, images, image_labels=[], **kwargs):
super().__init__(sections, **kwargs)
self.ID = TYPE_IMAGE
if len(images) != len(self.sections):
raise UserWarning(
'Number of images and sections for the ImagePlayer do not match')
self.images = images
if image_labels:
if len(image_labels) != len(self.sections):
raise UserWarning(
'Number of image labels and sections do not match')
self.image_labels = image_labels


class MatchingPairs(Multiplayer):
'''
This is a special case of multiplayer:
play buttons are represented as cards
'''

def __init__(self, sections, **kwargs):
super().__init__(sections, **kwargs)
self.ID = TYPE_MATCHINGPAIRS


class VisualMatchingPairs(MatchingPairs):
'''
This is a special case of multiplayer:
play buttons are represented as cards
this player does not play audio, but displays images instead
'''

def __init__(self, sections, **kwargs):
super().__init__(sections, **kwargs)
self.ID = TYPE_VISUALMATCHINGPAIRS
self.play_method = PLAY_NOAUDIO
48 changes: 20 additions & 28 deletions backend/experiment/actions/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.utils.translation import gettext as _

from .form import BooleanQuestion, ChoiceQuestion, Form
from .playback import Playback
from .playback import Autoplay, PlayButton
from .trial import Trial

from result.utils import prepare_result
Expand All @@ -17,9 +17,8 @@ def two_alternative_forced(session, section, choices, expected_response=None, st
Provide data for a Two Alternative Forced view that (auto)plays a section,
shows a question and has two customizable buttons
"""
playback = Playback(
[section],
'BUTTON'
playback = PlayButton(
[section]
)
key = 'choice'
button_style = {'invisible-text': True,
Expand All @@ -46,8 +45,9 @@ def two_alternative_forced(session, section, choices, expected_response=None, st
return trial


def song_sync(session, section, title, play_method='BUFFER',
recognition_time=15, sync_time=15, min_jitter=10, max_jitter=15):
def song_sync(session, section, title,
recognition_time=15, sync_time=15,
min_jitter=10, max_jitter=15):
trial_config = {
'response_time': recognition_time,
'auto_advance': True
Expand All @@ -59,31 +59,25 @@ def song_sync(session, section, title, play_method='BUFFER',
'recognize', session, section=section, scoring_rule='SONG_SYNC_RECOGNITION'),
submits=True
)]),
playback=Playback([section], 'AUTOPLAY', play_config={
'ready_time': 3,
'show_animation': True,
'play_method': play_method
},
preload_message=_('Get ready!'),
instruction=_('Do you recognize the song?'),
),
playback=Autoplay([section], show_animation=True,
ready_time=3,
preload_message=_('Get ready!'),
instruction=_('Do you recognize the song?'),
),
config={**trial_config,
'break_round_on': {'EQUALS': ['TIMEOUT', 'no']}},
title=title
)
silence_time = 4
silence = Trial(
playback=Playback([section], 'AUTOPLAY',
playback=Autoplay([section],
show_animation=True,
instruction=_('Keep imagining the music'),
play_config={
'mute': True,
'ready_time': 0,
'show_animation': True,
}),
mute=True),
config={
'response_time': silence_time,
'auto_advance': True,
'show_continue_button': False
'show_continue_button': False,
},
title=title
)
Expand All @@ -99,15 +93,13 @@ def song_sync(session, section, title, play_method='BUFFER',
scoring_rule='SONG_SYNC_CONTINUATION',
expected_response='yes' if continuation_correctness else 'no')
)]),
playback=Playback([section], 'AUTOPLAY',
playback=Autoplay([section],
instruction=_(
'Did the track come back in the right place?'),
play_config={
'ready_time': 0,
'playhead': randomize_playhead(min_jitter, max_jitter, silence_time, continuation_correctness),
'show_animation': True,
'resume_play': True
}),
show_animation=True,
play_from=randomize_playhead(
min_jitter, max_jitter, silence_time, continuation_correctness),
resume_play=True),
config=trial_config,
title=title
)
Expand Down
8 changes: 4 additions & 4 deletions backend/experiment/rules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .huang_2022 import Huang2022
from .kuiper_2020 import Kuiper2020
from .listening_conditions import ListeningConditions
from .matching_pairs import MatchingPairs
from .matching_pairs import MatchingPairsGame
from .matching_pairs_icmpc import MatchingPairsICMPC
from .musical_preferences import MusicalPreferences
from .rhythm_discrimination import RhythmDiscrimination
Expand All @@ -29,7 +29,7 @@
from .toontjehoger_4_absolute import ToontjeHoger4Absolute
from .toontjehoger_5_tempo import ToontjeHoger5Tempo
from .toontjehoger_6_relative import ToontjeHoger6Relative
from .visual_matching_pairs import VisualMatchingPairs
from .visual_matching_pairs import VisualMatchingPairsGame

# Rules available to this application
# If you create new Rules, add them to the list
Expand All @@ -46,7 +46,7 @@
BST.ID: BST,
Hooked.ID: Hooked,
HookedTeleTunes.ID: HookedTeleTunes,
MatchingPairs.ID: MatchingPairs,
MatchingPairsGame.ID: MatchingPairsGame,
MatchingPairsICMPC.ID: MatchingPairsICMPC,
MusicalPreferences.ID: MusicalPreferences,
RhythmDiscrimination.ID: RhythmDiscrimination,
Expand All @@ -67,5 +67,5 @@
Eurovision2020.ID: Eurovision2020,
Kuiper2020.ID: Kuiper2020,
ThatsMySong.ID: ThatsMySong,
VisualMatchingPairs.ID: VisualMatchingPairs
VisualMatchingPairsGame.ID: VisualMatchingPairsGame
}
4 changes: 2 additions & 2 deletions backend/experiment/rules/anisochrony.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from section.models import Section
from experiment.actions import Trial, Explainer, Step
from experiment.actions.form import ChoiceQuestion, Form
from experiment.actions.playback import Playback
from experiment.actions.playback import Autoplay
from experiment.actions.utils import render_feedback_trivia
from .duration_discrimination import DurationDiscrimination

Expand Down Expand Up @@ -65,7 +65,7 @@ def next_trial_action(self, session, trial_condition, difficulty):
submits=True
)

playback = Playback([section])
playback = Autoplay([section])
form = Form([question])
config = {
'listen_first': True,
Expand Down
6 changes: 3 additions & 3 deletions backend/experiment/rules/beat_alignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from .base import Base
from experiment.actions import Trial, Explainer, Consent, StartSession, Step
from experiment.actions.form import ChoiceQuestion, Form
from experiment.actions.playback import Playback
from experiment.actions.playback import Autoplay
from experiment.actions.utils import final_action_with_optional_button, render_feedback_trivia
from result.utils import prepare_result

Expand Down Expand Up @@ -108,7 +108,7 @@ def next_practice_action(self, playlist, count):
else:
presentation_text = _(
"In this example the beeps are NOT ALIGNED TO THE BEAT of the music.")
playback = Playback([section],
playback = Autoplay([section],
instruction=presentation_text,
preload_message=presentation_text,
)
Expand Down Expand Up @@ -145,7 +145,7 @@ def next_trial_action(self, session, this_round):
submits=True
)
form = Form([question])
playback = Playback([section])
playback = Autoplay([section])
view = Trial(
playback=playback,
feedback_form=form,
Expand Down
1 change: 0 additions & 1 deletion backend/experiment/rules/categorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from experiment.actions.form import Form, ChoiceQuestion
from experiment.actions import Consent, Explainer, Score, StartSession, Trial, Final
from experiment.actions.wrappers import two_alternative_forced
from experiment.questions.utils import unanswered_questions

from experiment.questions.demographics import EXTRA_DEMOGRAPHICS
from experiment.questions.utils import question_by_key
Expand Down
6 changes: 3 additions & 3 deletions backend/experiment/rules/duration_discrimination.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from section.models import Section
from experiment.actions import Trial, Consent, Explainer, StartSession, Step
from experiment.actions.form import ChoiceQuestion, Form
from experiment.actions.playback import Playback
from experiment.actions.playback import Autoplay
from experiment.actions.utils import final_action_with_optional_button, render_feedback_trivia
from experiment.actions.utils import get_average_difference
from experiment.rules.util.practice import get_trial_condition_block, get_practice_views, practice_explainer
Expand Down Expand Up @@ -140,8 +140,8 @@ def next_trial_action(self, session, trial_condition, difficulty):
submits=True
)
# create Result object and save expected result to database

playback = Playback([section])
playback = Autoplay([section])
form = Form([question])
view = Trial(
playback=playback,
Expand Down
Loading
Loading