diff --git a/backend/experiment/actions/playback.py b/backend/experiment/actions/playback.py
index faa18df80..9ab16ec6d 100644
--- a/backend/experiment/actions/playback.py
+++ b/backend/experiment/actions/playback.py
@@ -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
diff --git a/backend/experiment/actions/wrappers.py b/backend/experiment/actions/wrappers.py
index 934ad5cdb..4c03d81c4 100644
--- a/backend/experiment/actions/wrappers.py
+++ b/backend/experiment/actions/wrappers.py
@@ -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
@@ -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,
@@ -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
@@ -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
)
@@ -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
)
diff --git a/backend/experiment/rules/__init__.py b/backend/experiment/rules/__init__.py
index a51512aa0..20e4934a0 100644
--- a/backend/experiment/rules/__init__.py
+++ b/backend/experiment/rules/__init__.py
@@ -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
@@ -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
@@ -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,
@@ -67,5 +67,5 @@
Eurovision2020.ID: Eurovision2020,
Kuiper2020.ID: Kuiper2020,
ThatsMySong.ID: ThatsMySong,
- VisualMatchingPairs.ID: VisualMatchingPairs
+ VisualMatchingPairsGame.ID: VisualMatchingPairsGame
}
diff --git a/backend/experiment/rules/anisochrony.py b/backend/experiment/rules/anisochrony.py
index d887c6163..a4d0bdd88 100644
--- a/backend/experiment/rules/anisochrony.py
+++ b/backend/experiment/rules/anisochrony.py
@@ -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
@@ -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,
diff --git a/backend/experiment/rules/beat_alignment.py b/backend/experiment/rules/beat_alignment.py
index 367ea9270..d024059b5 100644
--- a/backend/experiment/rules/beat_alignment.py
+++ b/backend/experiment/rules/beat_alignment.py
@@ -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
@@ -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,
)
@@ -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,
diff --git a/backend/experiment/rules/categorization.py b/backend/experiment/rules/categorization.py
index 66ef5a5ed..e9666f4dc 100644
--- a/backend/experiment/rules/categorization.py
+++ b/backend/experiment/rules/categorization.py
@@ -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
diff --git a/backend/experiment/rules/duration_discrimination.py b/backend/experiment/rules/duration_discrimination.py
index 0a711503c..64c5bca37 100644
--- a/backend/experiment/rules/duration_discrimination.py
+++ b/backend/experiment/rules/duration_discrimination.py
@@ -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
@@ -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,
diff --git a/backend/experiment/rules/eurovision_2020.py b/backend/experiment/rules/eurovision_2020.py
index 398a28769..d632fa752 100644
--- a/backend/experiment/rules/eurovision_2020.py
+++ b/backend/experiment/rules/eurovision_2020.py
@@ -2,7 +2,7 @@
import random
from django.utils.translation import gettext_lazy as _
from experiment.actions import Trial
-from experiment.actions.playback import Playback
+from experiment.actions.playback import Autoplay
from experiment.actions.form import BooleanQuestion, Form
from experiment.actions.styles import STYLE_BOOLEAN_NEGATIVE_FIRST
from experiment.actions.wrappers import song_sync
@@ -139,12 +139,13 @@ def next_heard_before_action(self, session):
print("Warning: no heard_before section found")
section = session.playlist.get_section()
- playback = Playback(
- sections=[section],
- play_config={'ready_time': 3, 'show_animation': True,
- 'play_method': self.play_method},
- preload_message=_('Get ready!'))
- expected_result = novelty[round_number]
+ playback = Autoplay(
+ sections = [section],
+ show_animation=True,
+ ready_time=3,
+ preload_message=_('Get ready!')
+ )
+ expected_result=novelty[round_number]
# create Result object and save expected result to database
result_pk = prepare_result('heard_before', session, section=section,
expected_response=expected_result, scoring_rule='REACTION_TIME')
diff --git a/backend/experiment/rules/h_bat.py b/backend/experiment/rules/h_bat.py
index 440bbbe05..e13adcb89 100644
--- a/backend/experiment/rules/h_bat.py
+++ b/backend/experiment/rules/h_bat.py
@@ -7,7 +7,7 @@
from section.models import Section
from experiment.actions import Trial, Consent, Explainer, Playlist, Step, StartSession
from experiment.actions.form import ChoiceQuestion, Form
-from experiment.actions.playback import Playback
+from experiment.actions.playback import Autoplay
from experiment.rules.util.practice import get_practice_views, practice_explainer, get_trial_condition, get_trial_condition_block
from experiment.actions.utils import final_action_with_optional_button, render_feedback_trivia
@@ -105,7 +105,7 @@ def next_trial_action(self, session, trial_condition, level=1, *kwargs):
view='BUTTON_ARRAY',
submits=True
)
- playback = Playback([section])
+ playback = Autoplay([section])
form = Form([question])
view = Trial(
playback=playback,
diff --git a/backend/experiment/rules/hbat_bst.py b/backend/experiment/rules/hbat_bst.py
index b8a661c78..2e29241dd 100644
--- a/backend/experiment/rules/hbat_bst.py
+++ b/backend/experiment/rules/hbat_bst.py
@@ -3,7 +3,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 final_action_with_optional_button, render_feedback_trivia
from experiment.actions.utils import get_average_difference_level_based
from result.utils import prepare_result
@@ -62,7 +62,7 @@ def next_trial_action(self, session, trial_condition, level=1):
expected_response=expected_response, scoring_rule='CORRECTNESS'),
submits=True
)
- playback = Playback([section])
+ playback = Autoplay([section])
form = Form([question])
view = Trial(
playback=playback,
diff --git a/backend/experiment/rules/hooked.py b/backend/experiment/rules/hooked.py
index 951891622..af9317fb0 100644
--- a/backend/experiment/rules/hooked.py
+++ b/backend/experiment/rules/hooked.py
@@ -8,7 +8,7 @@
from .base import Base
from experiment.actions import Consent, Explainer, Final, Playlist, Score, StartSession, Step, Trial
from experiment.actions.form import BooleanQuestion, Form
-from experiment.actions.playback import Playback
+from experiment.actions.playback import Autoplay
from experiment.questions.demographics import DEMOGRAPHICS
from experiment.questions.goldsmiths import MSI_OTHER
from experiment.questions.utils import question_by_key
@@ -283,7 +283,7 @@ def next_song_sync_action(self, session, explainers=[]):
if not section:
logger.warning("Warning: no next_song_sync section found")
section = session.section_from_any_song()
- return song_sync(session, section, title=self.get_trial_title(session, round_number), play_method=self.play_method,
+ return song_sync(session, section, title=self.get_trial_title(session, round_number),
recognition_time=self.recognition_time, sync_time=self.sync_time,
min_jitter=self.min_jitter, max_jitter=self.max_jitter)
@@ -307,11 +307,12 @@ def next_heard_before_action(self, session):
if not section:
logger.warning("Warning: no heard_before section found")
section = session.section_from_any_song()
- playback = Playback(
+ playback = Autoplay(
[section],
- play_config={'ready_time': 3, 'show_animation': True,
- 'play_method': self.play_method},
- preload_message=_('Get ready!'))
+ show_animation=True,
+ ready_time=3,
+ preload_message=_('Get ready!')
+ )
expected_response = this_section_info.get('novelty')
# create Result object and save expected result to database
key = 'heard_before'
diff --git a/backend/experiment/rules/huang_2022.py b/backend/experiment/rules/huang_2022.py
index 17c0290de..89c1399ad 100644
--- a/backend/experiment/rules/huang_2022.py
+++ b/backend/experiment/rules/huang_2022.py
@@ -4,9 +4,9 @@
from django.template.loader import render_to_string
from django.conf import settings
-from experiment.actions import HTML, Final, Score, Explainer, Step, Consent, StartSession, Redirect, Playlist, Trial
-from experiment.actions.form import BooleanQuestion, ChoiceQuestion, Form, Question
-from experiment.actions.playback import Playback
+from experiment.actions import HTML, Final, Explainer, Step, Consent, StartSession, Redirect, Playlist, Trial
+from experiment.actions.form import BooleanQuestion, Form
+from experiment.actions.playback import Autoplay
from experiment.questions.demographics import EXTRA_DEMOGRAPHICS
from experiment.questions.goldsmiths import MSI_ALL, MSI_OTHER
from experiment.questions.other import OTHER
@@ -82,7 +82,7 @@ def next_round(self, session):
if not plan:
last_result = session.result_set.last()
if not last_result:
- playback = get_test_playback(self.play_method)
+ playback = get_test_playback()
html = HTML(body='
{}
'.format(_('Do you hear the music?')))
form = Form(form=[BooleanQuestion(
key='audio_check1',
@@ -98,7 +98,7 @@ def next_round(self, session):
if last_result.score == 0:
# user indicated they couldn't hear the music
if last_result.question_key == 'audio_check1':
- playback = get_test_playback(self.play_method)
+ playback = get_test_playback()
html = HTML(body=render_to_string('html/huang_2022/audio_check.html'))
form = Form(form=[BooleanQuestion(
key='audio_check2',
@@ -233,13 +233,12 @@ def final_score_message(self, session):
return " ".join([str(m) for m in messages])
-def get_test_playback(play_method):
+def get_test_playback():
from section.models import Section
test_section = Section.objects.get(song__name='audiocheck')
- playback = Playback(sections=[test_section],
- play_config={
- 'play_method': play_method,
- 'show_animation': True
- })
+ playback = Autoplay(
+ sections=[test_section],
+ show_animation=True
+ )
return playback
\ No newline at end of file
diff --git a/backend/experiment/rules/kuiper_2020.py b/backend/experiment/rules/kuiper_2020.py
index 1011e77aa..e661299d7 100644
--- a/backend/experiment/rules/kuiper_2020.py
+++ b/backend/experiment/rules/kuiper_2020.py
@@ -2,7 +2,7 @@
from django.utils.translation import gettext_lazy as _
from experiment.actions import Trial
-from experiment.actions.playback import Playback
+from experiment.actions.playback import Autoplay
from experiment.actions.form import BooleanQuestion, Form
from experiment.actions.styles import STYLE_BOOLEAN_NEGATIVE_FIRST
from experiment.actions.wrappers import song_sync
@@ -129,11 +129,13 @@ def next_heard_before_action(self, session):
print("Warning: no heard_before section found")
section = session.section_from_any_song()
- playback = Playback(
+ playback = Autoplay(
[section],
- play_config={'ready_time': 3, 'show_animation': True},
- preload_message=_('Get ready!'))
- expected_result = novelty[round_number]
+ show_animation=True,
+ ready_time=3,
+ preload_message=_('Get ready!')
+ )
+ expected_result=novelty[round_number]
# create Result object and save expected result to database
result_pk = prepare_result('heard_before', session, section=section,
expected_response=expected_result, scoring_rule='REACTION_TIME')
diff --git a/backend/experiment/rules/listening_conditions.py b/backend/experiment/rules/listening_conditions.py
index 067fe0b43..2be61a3af 100644
--- a/backend/experiment/rules/listening_conditions.py
+++ b/backend/experiment/rules/listening_conditions.py
@@ -2,9 +2,9 @@
from django.utils.translation import gettext_lazy as _
from .base import Base
-from experiment.actions import Consent, Explainer, Step, Playback, Playlist, StartSession, Trial
+from experiment.actions import Consent, Explainer, Step, Playlist, StartSession, Trial
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
from result.utils import prepare_result
@@ -87,7 +87,7 @@ def next_round(self, session, request_session=None):
instruction = _("You can now set the sound to a comfortable level. \
You can then adjust the volume to as high a level as possible without it being uncomfortable. \
When you are satisfied with the sound level, click Continue")
- playback = Playback([section], instruction=instruction)
+ playback = Autoplay([section], instruction=instruction)
message = _(
"Please keep the eventual sound level the same over the course of the experiment.")
actions = [
diff --git a/backend/experiment/rules/matching_pairs.py b/backend/experiment/rules/matching_pairs.py
index 7ab39f24f..a96f5aa70 100644
--- a/backend/experiment/rules/matching_pairs.py
+++ b/backend/experiment/rules/matching_pairs.py
@@ -5,7 +5,7 @@
from .base import Base
from experiment.actions import Consent, Explainer, Final, Playlist, StartSession, Step, Trial
-from experiment.actions.playback import Playback
+from experiment.actions.playback import MatchingPairs
from experiment.questions.demographics import EXTRA_DEMOGRAPHICS
from experiment.questions.utils import question_by_key
from result.utils import prepare_result
@@ -13,7 +13,7 @@
from section.models import Section
-class MatchingPairs(Base):
+class MatchingPairsGame(Base):
ID = 'MATCHING_PAIRS'
num_pairs = 8
contact_email = 'aml.tunetwins@gmail.com'
@@ -108,10 +108,9 @@ def get_matching_pairs_trial(self, session):
degradations = session.playlist.section_set.filter(group__in=selected_pairs, tag=degradation_type)
player_sections = list(originals) + list(degradations)
random.shuffle(player_sections)
- playback = Playback(
+ playback = MatchingPairs(
sections=player_sections,
- player_type='MATCHINGPAIRS',
- play_config={'stop_audio_after': 5}
+ stop_audio_after=5
)
trial = Trial(
title='Tune twins',
diff --git a/backend/experiment/rules/musical_preferences.py b/backend/experiment/rules/musical_preferences.py
index 464d2ab3f..6ae1fc590 100644
--- a/backend/experiment/rules/musical_preferences.py
+++ b/backend/experiment/rules/musical_preferences.py
@@ -10,7 +10,7 @@
from experiment.actions import Consent, Explainer, Final, HTML, Playlist, Redirect, Step, StartSession, Trial
from experiment.actions.form import BooleanQuestion, ChoiceQuestion, Form, LikertQuestionIcon
-from experiment.actions.playback import Playback
+from experiment.actions.playback import Autoplay
from experiment.actions.styles import STYLE_BOOLEAN, STYLE_BOOLEAN_NEGATIVE_FIRST
from result.utils import prepare_result
@@ -111,9 +111,8 @@ def next_round(self, session, request_session=None):
else:
session.decrement_round()
if last_result.question_key == 'audio_check1':
- playback = get_test_playback('EXTERNAL')
- html = HTML(body=render_to_string(
- 'html/huang_2022/audio_check.html'))
+ playback = get_test_playback()
+ html = HTML(body=render_to_string('html/huang_2022/audio_check.html'))
form = Form(form=[BooleanQuestion(
key='audio_check2',
choices={'no': _('Quit'), 'yes': _('Next')},
@@ -132,7 +131,7 @@ def next_round(self, session, request_session=None):
return Redirect(settings.HOMEPAGE)
else:
session.decrement_round()
- playback = get_test_playback('EXTERNAL')
+ playback = get_test_playback()
html = HTML(
body='{}
'.format(_('Do you hear the music?')))
form = Form(form=[BooleanQuestion(
@@ -218,7 +217,7 @@ def next_round(self, session, request_session=None):
result_id=prepare_result(know_key, session, section=section),
style=STYLE_BOOLEAN
)
- playback = Playback([section], play_config={'show_animation': True})
+ playback = Autoplay([section], show_animation=True)
form = Form([know, likert])
view = Trial(
playback=playback,
diff --git a/backend/experiment/rules/rhythm_discrimination.py b/backend/experiment/rules/rhythm_discrimination.py
index 25ede35ca..f89a423fa 100644
--- a/backend/experiment/rules/rhythm_discrimination.py
+++ b/backend/experiment/rules/rhythm_discrimination.py
@@ -6,7 +6,7 @@
from experiment.actions.utils import final_action_with_optional_button, render_feedback_trivia
from experiment.rules.util.practice import practice_explainer, practice_again_explainer, start_experiment_explainer
from experiment.actions import Trial, Consent, Explainer, StartSession, Step
-from experiment.actions.playback import Playback
+from experiment.actions.playback import Autoplay
from experiment.actions.form import ChoiceQuestion, Form
from result.utils import prepare_result
@@ -173,7 +173,7 @@ def next_trial_actions(session, round_number, request_session):
submits=True
)
form = Form([question])
- playback = Playback([section])
+ playback = Autoplay([section])
if round_number < 5:
title = _('practice')
else:
diff --git a/backend/experiment/rules/speech2song.py b/backend/experiment/rules/speech2song.py
index 6ace2d778..4d308b5ea 100644
--- a/backend/experiment/rules/speech2song.py
+++ b/backend/experiment/rules/speech2song.py
@@ -7,7 +7,7 @@
from experiment.actions import Consent, Explainer, Step, Final, Playlist, Trial, StartSession
from experiment.actions.form import Form, RadiosQuestion
-from experiment.actions.playback import Playback
+from experiment.actions.playback import Autoplay
from experiment.questions.demographics import EXTRA_DEMOGRAPHICS
from experiment.questions.languages import LANGUAGE, LanguageQuestion
from experiment.questions.utils import question_by_key
@@ -232,15 +232,10 @@ def sound(section, n_representation=None):
ready_time = 0
else:
ready_time = 1
- config = {
- 'ready_time': ready_time,
- 'show_animation': False
- }
title = _('Listen carefully')
- playback = Playback(
+ playback = Autoplay(
sections = [section],
- player_type='AUTOPLAY',
- play_config=config
+ ready_time = ready_time,
)
view = Trial(
playback=playback,
diff --git a/backend/experiment/rules/tests/test_hooked.py b/backend/experiment/rules/tests/test_hooked.py
index 76866bc6c..273dc4538 100644
--- a/backend/experiment/rules/tests/test_hooked.py
+++ b/backend/experiment/rules/tests/test_hooked.py
@@ -1,7 +1,6 @@
from django.test import TestCase
from experiment.models import Experiment
-from experiment.rules import Eurovision2020, Huang2022, ThatsMySong
from experiment.questions.musicgens import MUSICGENS_17_W_VARIANTS
from participant.models import Participant
from result.models import Result
diff --git a/backend/experiment/rules/tests/test_matching_pairs.py b/backend/experiment/rules/tests/test_matching_pairs.py
index 3b26e1183..b2644a816 100644
--- a/backend/experiment/rules/tests/test_matching_pairs.py
+++ b/backend/experiment/rules/tests/test_matching_pairs.py
@@ -1,7 +1,6 @@
from django.test import TestCase
from experiment.models import Experiment
-from experiment.rules import MatchingPairs
from participant.models import Participant
from section.models import Playlist
from session.models import Session
diff --git a/backend/experiment/rules/tests/test_visual_matching_pairs.py b/backend/experiment/rules/tests/test_visual_matching_pairs.py
index d68e2abfa..fa95af2ab 100644
--- a/backend/experiment/rules/tests/test_visual_matching_pairs.py
+++ b/backend/experiment/rules/tests/test_visual_matching_pairs.py
@@ -1,7 +1,7 @@
from django.test import TestCase
from experiment.models import Experiment
-from experiment.rules import VisualMatchingPairs
+from experiment.rules import VisualMatchingPairsGame
from participant.models import Participant
from section.models import Playlist
from session.models import Session
@@ -35,7 +35,7 @@ def setUpTestData(self):
participant=self.participant,
playlist=self.playlist
)
- self.rules = VisualMatchingPairs()
+ self.rules = VisualMatchingPairsGame()
def test_visual_matching_pairs_trial(self):
trial = self.rules.get_visual_matching_pairs_trial(self.session)
diff --git a/backend/experiment/rules/toontjehoger_1_mozart.py b/backend/experiment/rules/toontjehoger_1_mozart.py
index e1524fd8f..b104c40bb 100644
--- a/backend/experiment/rules/toontjehoger_1_mozart.py
+++ b/backend/experiment/rules/toontjehoger_1_mozart.py
@@ -3,7 +3,7 @@
from os.path import join
from experiment.actions import Trial, Explainer, Step, Score, Final, StartSession, Playlist, Info, HTML
from experiment.actions.form import ButtonArrayQuestion, Form
-from experiment.actions.playback import Playback
+from experiment.actions.playback import Autoplay
from .base import Base
from experiment.utils import non_breaking_spaces
@@ -141,11 +141,7 @@ def get_image_trial(self, session, section_group, image_url, question, expected_
# --------------------
# Listen
- play_config = {'show_animation': True}
- playback = Playback([section],
- player_type=Playback.TYPE_AUTOPLAY,
- play_config=play_config
- )
+ playback = Autoplay([section], show_animation=True)
listen_config = {
'auto_advance': True,
diff --git a/backend/experiment/rules/toontjehoger_2_preverbal.py b/backend/experiment/rules/toontjehoger_2_preverbal.py
index 4aa159443..67aef7687 100644
--- a/backend/experiment/rules/toontjehoger_2_preverbal.py
+++ b/backend/experiment/rules/toontjehoger_2_preverbal.py
@@ -4,7 +4,7 @@
from .toontjehoger_1_mozart import toontjehoger_ranks
from experiment.actions import Trial, Explainer, Step, Score, Final, StartSession, Playlist, Info, HTML
from experiment.actions.form import ButtonArrayQuestion, ChoiceQuestion, Form
-from experiment.actions.playback import Playback
+from experiment.actions.playback import ImagePlayer
from experiment.actions.styles import STYLE_NEUTRAL
from .base import Base
from os.path import join
@@ -159,13 +159,12 @@ def get_round1_playback(self, session):
"Error: could not find section C for round 1")
# Player
- play_config = {
- 'label_style': 'ALPHABETIC',
- 'spectrograms': ["/images/experiments/toontjehoger/spectrogram-trumpet.webp", "/images/experiments/toontjehoger/spectrogram-whale.webp", "/images/experiments/toontjehoger/spectrogram-human.webp"],
- 'spectrogram_labels': ['Trompet', 'Walvis', 'Mens'],
- }
- playback = Playback(
- [sectionA, sectionB, sectionC], player_type=Playback.TYPE_SPECTROGRAM, play_config=play_config)
+ playback = ImagePlayer(
+ [sectionA, sectionB, sectionC],
+ label_style='ALPHABETIC',
+ images=["/images/experiments/toontjehoger/spectrogram-trumpet.webp", "/images/experiments/toontjehoger/spectrogram-whale.webp", "/images/experiments/toontjehoger/spectrogram-human.webp"],
+ image_labels = ['Trompet', 'Walvis', 'Mens']
+ )
trial = Trial(
playback=playback,
@@ -192,12 +191,11 @@ def get_round2(self, round, session):
"Error: could not find section B for round 2")
# Player
- play_config = {
- 'label_style': 'ALPHABETIC',
- 'spectrograms': ["/images/experiments/toontjehoger/spectrogram-baby-french.webp", "/images/experiments/toontjehoger/spectrogram-baby-german.webp"]
- }
- playback = Playback(
- [sectionA, sectionB], player_type=Playback.TYPE_SPECTROGRAM, play_config=play_config)
+ playback = ImagePlayer(
+ [sectionA, sectionB],
+ label_style='ALPHABETIC',
+ images=["/images/experiments/toontjehoger/spectrogram-baby-french.webp", "/images/experiments/toontjehoger/spectrogram-baby-german.webp"],
+ )
# Question
key = 'baby'
diff --git a/backend/experiment/rules/toontjehoger_3_plink.py b/backend/experiment/rules/toontjehoger_3_plink.py
index d6ed76a90..dad0327f0 100644
--- a/backend/experiment/rules/toontjehoger_3_plink.py
+++ b/backend/experiment/rules/toontjehoger_3_plink.py
@@ -3,7 +3,8 @@
from django.template.loader import render_to_string
from .toontjehoger_1_mozart import toontjehoger_ranks
-from experiment.actions import Explainer, Step, Score, Final, StartSession, Playback, Playlist, Info, Trial
+from experiment.actions import Explainer, Step, Score, Final, StartSession, Playlist, Info, Trial
+from experiment.actions.playback import PlayButton
from experiment.actions.form import AutoCompleteQuestion, RadiosQuestion, Form
from .base import Base
@@ -95,9 +96,11 @@ def get_score_view(self, session):
if len(last_results) == 1:
# plink result
if last_results[0].expected_response == last_results[0].given_response:
- feedback = "Goedzo! Je hoorde inderdaad {} van {}.".format(non_breaking_spaces(section.song.name), non_breaking_spaces(section.song.artist))
+ feedback = "Goedzo! Je hoorde inderdaad {} van {}.".format(
+ non_breaking_spaces(section.song.name), non_breaking_spaces(section.song.artist))
else:
- feedback = "Helaas! Je hoorde {} van {}.".format(non_breaking_spaces(section.song.name), non_breaking_spaces(section.song.artist))
+ feedback = "Helaas! Je hoorde {} van {}.".format(non_breaking_spaces(
+ section.song.name), non_breaking_spaces(section.song.artist))
else:
if score == 2 * self.SCORE_EXTRA_WRONG:
feedback_prefix = "Helaas!"
@@ -125,7 +128,7 @@ def get_score_view(self, session):
config = {'show_total_score': True}
round_number = session.get_relevant_results(['plink']).count() - 1
- score_title = "Ronde %(number)d / %(total)d" %\
+ score_title = "Ronde %(number)d / %(total)d" %\
{'number': round_number+1, 'total': session.experiment.rounds}
return Score(session, config=config, feedback=feedback, score=score, title=score_title)
@@ -159,8 +162,7 @@ def get_plink_round(self, session, present_score=False):
)
)
next_round.append(Trial(
- playback=Playback(
- player_type='BUTTON',
+ playback=PlayButton(
sections=[section]
),
feedback_form=Form(
@@ -247,7 +249,8 @@ def calculate_score(self, result, data):
if result.question_key == 'plink':
return self.SCORE_MAIN_CORRECT if result.expected_response == result.given_response else self.SCORE_MAIN_WRONG
elif result.question_key == 'era':
- result.session.save_json_data({'extra_questions_intro_shown': True})
+ result.session.save_json_data(
+ {'extra_questions_intro_shown': True})
result.session.save()
return self.SCORE_EXTRA_1_CORRECT if result.given_response == result.expected_response else self.SCORE_EXTRA_WRONG
else:
diff --git a/backend/experiment/rules/toontjehoger_4_absolute.py b/backend/experiment/rules/toontjehoger_4_absolute.py
index adde4623b..7f899931b 100644
--- a/backend/experiment/rules/toontjehoger_4_absolute.py
+++ b/backend/experiment/rules/toontjehoger_4_absolute.py
@@ -6,8 +6,9 @@
from .toontjehoger_1_mozart import toontjehoger_ranks
from experiment.actions import Trial, Explainer, Step, Score, Final, StartSession, Playlist, Info
from experiment.actions.form import ButtonArrayQuestion, Form
-from experiment.actions.playback import Playback
+from experiment.actions.playback import Multiplayer
from experiment.actions.styles import STYLE_NEUTRAL
+from experiment.utils import create_player_labels
from .base import Base
from result.utils import prepare_result
@@ -97,12 +98,7 @@ def get_round(self, session):
random.shuffle(sections)
# Player
- play_config = {
- 'label_style': 'ALPHABETIC',
- }
-
- playback = Playback(
- sections, player_type=Playback.TYPE_MULTIPLAYER, play_config=play_config)
+ playback = Multiplayer(sections, labels=create_player_labels(len(sections), 'alphabetic'))
# Question
key = 'pitch'
diff --git a/backend/experiment/rules/toontjehoger_5_tempo.py b/backend/experiment/rules/toontjehoger_5_tempo.py
index 1fdc2377c..a1210edbc 100644
--- a/backend/experiment/rules/toontjehoger_5_tempo.py
+++ b/backend/experiment/rules/toontjehoger_5_tempo.py
@@ -5,10 +5,10 @@
from .toontjehoger_1_mozart import toontjehoger_ranks
from experiment.actions import Trial, Explainer, Step, Score, Final, StartSession, Playlist, Info
from experiment.actions.form import ButtonArrayQuestion, Form
-from experiment.actions.playback import Playback
+from experiment.actions.playback import Multiplayer
from experiment.actions.styles import STYLE_NEUTRAL
from .base import Base
-from experiment.utils import non_breaking_spaces
+from experiment.utils import create_player_labels, non_breaking_spaces
from result.utils import prepare_result
@@ -136,12 +136,7 @@ def get_round(self, session, round):
section_original = sections[0] if sections[0].group == "or" else sections[1]
# Player
- play_config = {
- 'label_style': 'ALPHABETIC',
- }
-
- playback = Playback(
- sections, player_type=Playback.TYPE_MULTIPLAYER, play_config=play_config)
+ playback = Multiplayer(sections, labels=create_player_labels(len(sections), 'alphabetic'))
# Question
key = 'pitch'
diff --git a/backend/experiment/rules/toontjehoger_6_relative.py b/backend/experiment/rules/toontjehoger_6_relative.py
index 02630b1be..56d976246 100644
--- a/backend/experiment/rules/toontjehoger_6_relative.py
+++ b/backend/experiment/rules/toontjehoger_6_relative.py
@@ -4,7 +4,7 @@
from .toontjehoger_1_mozart import toontjehoger_ranks
from experiment.actions import Trial, Explainer, Step, Score, Final, StartSession, Playlist, Info
from experiment.actions.form import ChoiceQuestion, Form
-from experiment.actions.playback import Playback
+from experiment.actions.playback import Multiplayer
from experiment.actions.styles import STYLE_BOOLEAN
from .base import Base
@@ -126,13 +126,11 @@ def get_round(self, round, session):
form = Form([question])
# Player
- play_config = {
- 'label_style': 'CUSTOM',
- 'labels': ['A', 'B' if round == 0 else 'C'],
- 'play_once': True,
- }
- playback = Playback(
- [section1, section2], player_type=Playback.TYPE_MULTIPLAYER, play_config=play_config)
+ playback = Multiplayer(
+ [section1, section2],
+ play_once=True,
+ labels=['A', 'B' if round == 0 else 'C']
+ )
trial = Trial(
playback=playback,
diff --git a/backend/experiment/rules/visual_matching_pairs.py b/backend/experiment/rules/visual_matching_pairs.py
index 25ca1ecd4..273bc95ed 100644
--- a/backend/experiment/rules/visual_matching_pairs.py
+++ b/backend/experiment/rules/visual_matching_pairs.py
@@ -5,7 +5,7 @@
from .base import Base
from experiment.actions import Consent, Explainer, Final, Playlist, StartSession, Step, Trial
-from experiment.actions.playback import Playback
+from experiment.actions.playback import VisualMatchingPairs
from experiment.questions.demographics import EXTRA_DEMOGRAPHICS
from experiment.questions.utils import question_by_key
from result.utils import prepare_result
@@ -13,7 +13,7 @@
from section.models import Section
-class VisualMatchingPairs(Base):
+class VisualMatchingPairsGame(Base):
ID = 'VISUAL_MATCHING_PAIRS'
num_pairs = 8
contact_email = 'aml.tunetwins@gmail.com'
@@ -92,10 +92,8 @@ def get_visual_matching_pairs_trial(self, session):
player_sections = list(session.playlist.section_set.filter(tag__contains='vmp'))
random.shuffle(player_sections)
- playback = Playback(
- sections=player_sections,
- player_type='VISUALMATCHINGPAIRS',
- play_config={ 'play_method': 'PREFETCH' }
+ playback = VisualMatchingPairs(
+ sections=player_sections
)
trial = Trial(
title='Visual Tune twins',
diff --git a/backend/experiment/tests/test_utils.py b/backend/experiment/tests/test_utils.py
new file mode 100644
index 000000000..0ae83aea7
--- /dev/null
+++ b/backend/experiment/tests/test_utils.py
@@ -0,0 +1,14 @@
+from django.test import TestCase
+
+from experiment.utils import create_player_labels
+
+
+class TestExperimentUtils(TestCase):
+
+ def test_create_player_labels(self):
+ labels = create_player_labels(3, 'alphabetic')
+ assert labels == ['A', 'B', 'C']
+ labels = create_player_labels(4, 'roman')
+ assert labels == ['I', 'II', 'III', 'IV']
+ labels = create_player_labels(2)
+ assert labels == ['1', '2']
\ No newline at end of file
diff --git a/backend/experiment/utils.py b/backend/experiment/utils.py
index ed41e8fbd..17dcaaa89 100644
--- a/backend/experiment/utils.py
+++ b/backend/experiment/utils.py
@@ -1,3 +1,5 @@
+import roman
+
def serialize(actions):
''' Serialize an array of actions '''
@@ -28,3 +30,16 @@ def non_breaking_spaces(s):
def external_url(text, url):
# Create a HTML element for an external url
return '{}'.format(url, text)
+
+
+def create_player_labels(num_labels, label_style='number'):
+ return [format_label(i, label_style) for i in range(num_labels)]
+
+
+def format_label(number, label_style):
+ if label_style == 'alphabetic':
+ return chr(number + 65)
+ elif label_style == 'roman':
+ return roman.toRoman(number+1)
+ else:
+ return str(number+1)
diff --git a/backend/requirements.in/base.txt b/backend/requirements.in/base.txt
index 30aeb7e60..8655debdb 100644
--- a/backend/requirements.in/base.txt
+++ b/backend/requirements.in/base.txt
@@ -20,6 +20,9 @@ IPToCC
# PostgrSQL database client
psycopg2
+# to convert labels to Roman numerals
+roman
+
# Sentry SDK, for monitoring performance & errors. See also sentry.io.
sentry-sdk
diff --git a/backend/requirements/dev.txt b/backend/requirements/dev.txt
index 6f30e9e93..27c59c4bd 100644
--- a/backend/requirements/dev.txt
+++ b/backend/requirements/dev.txt
@@ -1,6 +1,6 @@
#
-# This file is autogenerated by pip-compile with Python 3.8
-# by the following command:
+# This file is autogenerated by pip-compile with python 3.8
+# To update, run:
#
# pip-compile --output-file=requirements/dev.txt requirements.in/dev.txt
#
@@ -101,6 +101,7 @@ requests==2.31.0
# via
# -r requirements.in/dev.txt
# genbadge
+roman==4.1
sentry-sdk==1.38.0
# via -r requirements.in/base.txt
six==1.16.0
diff --git a/backend/requirements/prod.txt b/backend/requirements/prod.txt
index 81daf2f11..0d03396db 100644
--- a/backend/requirements/prod.txt
+++ b/backend/requirements/prod.txt
@@ -1,6 +1,6 @@
#
-# This file is autogenerated by pip-compile with Python 3.8
-# by the following command:
+# This file is autogenerated by pip-compile with python 3.8
+# To update, run:
#
# pip-compile --output-file=requirements/prod.txt requirements.in/prod.txt
#
@@ -55,6 +55,7 @@ pytz==2023.3
# pandas
requests==2.31.0
# via genbadge
+roman==4.1
sentry-sdk==1.38.0
# via -r requirements.in/base.txt
six==1.16.0
diff --git a/frontend/src/components/Playback/Autoplay.js b/frontend/src/components/Playback/Autoplay.js
index 10bb080a0..f58d76c5e 100644
--- a/frontend/src/components/Playback/Autoplay.js
+++ b/frontend/src/components/Playback/Autoplay.js
@@ -1,17 +1,16 @@
-import React, { useRef, useEffect } from "react";
+import React, { useEffect, useState } from "react";
import Circle from "../Circle/Circle";
import ListenCircle from "../ListenCircle/ListenCircle";
-const AutoPlay = ({instruction, playConfig, playSection, time, startedPlaying, finishedPlaying, responseTime, className=''}) => {
- // player state
+const AutoPlay = ({instruction, showAnimation, playSection, startedPlaying, finishedPlaying, responseTime, className=''}) => {
- const running = useRef(playConfig.auto_play);
+ const [running, setRunning] = useState(true);
// Handle view logic
useEffect(() => {
playSection(0)
- }, [playConfig, startedPlaying]);
+ }, [playSection, startedPlaying]);
// Render component
return (
@@ -21,14 +20,15 @@ const AutoPlay = ({instruction, playConfig, playSection, time, startedPlaying, f
running={running}
duration={responseTime}
color="white"
- animateCircle={playConfig.show_animation}
+ animateCircle={showAnimation}
onFinish={() => {
// Stop audio
+ setRunning(false);
finishedPlaying();
}}
/>
- {playConfig.show_animation
+ {showAnimation
?
{
+const ImagePlayer = (props) => {
const playSection = props.playSection;
// extraContent callback can be used to add content to each player
const extraContent = useCallback(
(index) => {
- const spectrograms = props.playConfig.spectrograms;
- if (!spectrograms) {
- return Warning: No spectrograms found
;
+ const images = props.images;
+ if (!images) {
+ return Warning: No images found
;
}
- const labels = props.playConfig.spectrogram_labels;
+ const labels = props.image_labels;
- if (index >= 0 && index < spectrograms.length) {
+ if (index >= 0 && index < images.length) {
return (
-
+
{
playSection(index);
}}
@@ -32,14 +32,14 @@ const SpectrogramPlayer = (props) => {
return
Warning: No spectrograms available for index {index}
;
}
},
- [props.playConfig.spectrograms, props.playConfig.spectrogram_labels, playSection]
+ [props.images, props.image_labels, playSection]
);
return (
-
+
);
};
-export default SpectrogramPlayer;
+export default ImagePlayer;
diff --git a/frontend/src/components/Playback/SpectrogramPlayer.scss b/frontend/src/components/Playback/ImagePlayer.scss
similarity index 97%
rename from frontend/src/components/Playback/SpectrogramPlayer.scss
rename to frontend/src/components/Playback/ImagePlayer.scss
index 726896acb..a037c6530 100644
--- a/frontend/src/components/Playback/SpectrogramPlayer.scss
+++ b/frontend/src/components/Playback/ImagePlayer.scss
@@ -1,4 +1,4 @@
-.aha__spectrogram-player {
+.aha__image-player {
max-width: 100vw;
.player-wrapper {
@@ -10,7 +10,7 @@
margin-bottom: 0;
}
- .spectrogram {
+ .image {
max-width: 400px;
width: calc(100% - 160px);
height: 100px;
diff --git a/frontend/src/components/Playback/MatchingPairs.js b/frontend/src/components/Playback/MatchingPairs.js
index f38431d21..0a1f65418 100644
--- a/frontend/src/components/Playback/MatchingPairs.js
+++ b/frontend/src/components/Playback/MatchingPairs.js
@@ -7,8 +7,8 @@ const MatchingPairs = ({
playSection,
sections,
playerIndex,
- finishedPlaying,
stopAudioAfter,
+ finishedPlaying,
submitResult,
}) => {
const xPosition = useRef(-1);
diff --git a/frontend/src/components/Playback/MatchingPairs.test.js b/frontend/src/components/Playback/MatchingPairs.test.js
index 4ceac1208..dbc35fb9f 100644
--- a/frontend/src/components/Playback/MatchingPairs.test.js
+++ b/frontend/src/components/Playback/MatchingPairs.test.js
@@ -12,7 +12,8 @@ describe('MatchingPairs Component', () => {
playSection: jest.fn(),
playerIndex: 0,
finishedPlaying: jest.fn(),
- stopAudioAfter: jest.fn(),
+ onFinish: jest.fn(),
+ stopAudioAfter: 4.0,
submitResult: jest.fn(),
};
diff --git a/frontend/src/components/Playback/MultiPlayer.js b/frontend/src/components/Playback/MultiPlayer.js
index 4029d516f..b7cff4418 100644
--- a/frontend/src/components/Playback/MultiPlayer.js
+++ b/frontend/src/components/Playback/MultiPlayer.js
@@ -2,13 +2,11 @@ import React from "react";
import PlayerSmall from "../PlayButton/PlayerSmall";
import classNames from "classnames";
-import { getPlayerLabel } from "../../util/label";
-
const MultiPlayer = ({
playSection,
sections,
playerIndex,
- playConfig,
+ labels,
disabledPlayers,
extraContent,
}) => {
@@ -30,13 +28,7 @@ const MultiPlayer = ({
disabledPlayers.includes(parseInt(index))
}
label={
- playConfig.label_style
- ? getPlayerLabel(
- index,
- playConfig.label_style,
- playConfig.labels || []
- )
- : ""
+ labels? labels[index] : ""
}
playing={playerIndex === index}
/>
diff --git a/frontend/src/components/Playback/Playback.js b/frontend/src/components/Playback/Playback.js
index 345c75dc9..535721dab 100644
--- a/frontend/src/components/Playback/Playback.js
+++ b/frontend/src/components/Playback/Playback.js
@@ -7,7 +7,7 @@ import { playAudio, pauseAudio } from "../../util/audioControl";
import AutoPlay from "./Autoplay";
import PlayButton from "../PlayButton/PlayButton";
import MultiPlayer from "./MultiPlayer";
-import SpectrogramPlayer from "./SpectrogramPlayer";
+import ImagePlayer from "./ImagePlayer";
import MatchingPairs from "./MatchingPairs";
import Preload from "../Preload/Preload";
import VisualMatchingPairs from "components/VisualMatchingPairs/VisualMatchingPairs";
@@ -15,20 +15,16 @@ import VisualMatchingPairs from "components/VisualMatchingPairs/VisualMatchingPa
export const AUTOPLAY = "AUTOPLAY";
export const BUTTON = "BUTTON";
export const MULTIPLAYER = "MULTIPLAYER";
-export const SPECTROGRAM = "SPECTROGRAM";
+export const IMAGE = "IMAGE";
export const MATCHINGPAIRS = "MATCHINGPAIRS";
export const PRELOAD = "PRELOAD";
export const VISUALMATCHINGPAIRS = "VISUALMATCHINGPAIRS";
const Playback = ({
- playerType,
- sections,
- instruction,
+ playbackArgs,
onPreloadReady,
- preloadMessage = '',
autoAdvance,
responseTime,
- playConfig = {},
submitResult,
startedPlaying,
finishedPlaying,
@@ -40,6 +36,8 @@ const Playback = ({
const setView = (view, data = {}) => {
setState({ view, ...data });
}
+ const playMethod = playbackArgs.play_method;
+ const sections = playbackArgs.sections;
// Keep track of which player has played, in a an array of player indices
const [hasPlayed, setHasPlayed] = useState([]);
@@ -71,106 +69,92 @@ const Playback = ({
const onAudioEnded = useCallback(() => {
setPlayerIndex(-1);
- //AJ: added for categorization experiment for form activation after playback and auto_advance to work properly
- if (playConfig.timeout_after_playback) {
- setTimeout(finishedPlaying, playConfig.timeout_after_playback);
+ if (playbackArgs.timeout_after_playback) {
+ setTimeout(finishedPlaying, playbackArgs.timeout_after_playback);
} else {
finishedPlaying();
}
- }, []);
-
+ }, [playbackArgs, finishedPlaying]);
+
// Keep track of last player index
useEffect(() => {
lastPlayerIndex.current = playerIndex;
}, [playerIndex]);
- if (playConfig.play_method === 'EXTERNAL') {
- webAudio.closeWebAudio();
+ if (playMethod === 'EXTERNAL') {
+ webAudio.closeWebAudio();
}
- // Play section with given index
- const playSection = useCallback(
- (index = 0) => {
-
- if (index !== lastPlayerIndex.current) {
- // Load different audio
- if (prevPlayerIndex.current !== -1) {
- pauseAudio(playConfig);
- }
- // Store player index
- setPlayerIndex(index);
-
- // Determine if audio should be played
- if (playConfig.mute) {
- setPlayerIndex(-1);
- pauseAudio(playConfig);
- return;
- }
- const playheadShift = getPlayheadShift();
- let latency = playAudio(playConfig, sections[index], playheadShift);
-
- // Cancel active events
- cancelAudioListeners();
-
- // listen for active audio events
- if (playConfig.play_method === 'BUFFER') {
- activeAudioEndedListener.current = webAudio.listenOnce("ended", onAudioEnded);
- } else {
- activeAudioEndedListener.current = audio.listenOnce("ended", onAudioEnded);
- }
-
- // Compensate for audio latency and set state to playing
- setTimeout(startedPlaying && startedPlaying(), latency);
- return;
- }
-
- // Stop playback
- if (lastPlayerIndex.current === index) {
- pauseAudio(playConfig);
- setPlayerIndex(-1);
- return;
- }
- },
- [playAudio, pauseAudio, sections, activeAudioEndedListener, cancelAudioListeners, startedPlaying, onAudioEnded]
- );
-
- const getPlayheadShift = () => {
+ const getPlayheadShift = useCallback(() => {
/* if the current Playback view has resume_play set to true,
retrieve previous Playback view's decisionTime from sessionStorage
*/
- return playConfig.resume_play ?
+ return playbackArgs.resume_play ?
parseFloat(window.sessionStorage.getItem('decisionTime')) : 0;
- }
+ }, [playbackArgs]
+ )
+
+ // Play section with given index
+ const playSection = useCallback((index = 0) => {
+ if (index !== lastPlayerIndex.current) {
+ // Load different audio
+ if (prevPlayerIndex.current !== -1) {
+ pauseAudio(playMethod);
+ }
+ // Store player index
+ setPlayerIndex(index);
+ // Determine if audio should be played
+ if (playbackArgs.mute) {
+ setPlayerIndex(-1);
+ pauseAudio(playMethod);
+ return;
+ }
+
+ const playheadShift = getPlayheadShift();
+ let latency = playAudio(sections[index], playMethod, playheadShift + playbackArgs.play_from);
+ // Cancel active events
+ cancelAudioListeners();
+ // listen for active audio events
+ if (playMethod === 'BUFFER') {
+ activeAudioEndedListener.current = webAudio.listenOnce("ended", onAudioEnded);
+ } else {
+ activeAudioEndedListener.current = audio.listenOnce("ended", onAudioEnded);
+ }
+ // Compensate for audio latency and set state to playing
+ setTimeout(startedPlaying && startedPlaying(), latency);
+ return;
+ }
+ // Stop playback
+ if (lastPlayerIndex.current === index) {
+ pauseAudio(playMethod);
+ setPlayerIndex(-1);
+ return;
+ }
+ }, [sections, activeAudioEndedListener, cancelAudioListeners, getPlayheadShift, playbackArgs, playMethod, startedPlaying, onAudioEnded]
+ );
// Local logic for onfinished playing
const onFinishedPlaying = useCallback(() => {
setPlayerIndex(-1);
- pauseAudio(playConfig);
+ pauseAudio(playMethod);
finishedPlaying && finishedPlaying();
- }, [finishedPlaying]);
+ }, [finishedPlaying, playMethod]);
// Stop audio on unmount
useEffect(
- () => () => {
- pauseAudio(playConfig);
+ () => {
+ return () => pauseAudio(playMethod);
},
- []
+ [playMethod]
);
- // Autoplay
- useEffect(() => {
- playConfig.auto_play && playSection(0);
- }, [playConfig.auto_play, playSection]);
-
const render = (view) => {
const attrs = {
sections,
+ showAnimation: playbackArgs.show_animation,
setView,
- instruction,
- preloadMessage,
autoAdvance,
responseTime,
- playConfig,
startedPlaying,
playerIndex,
finishedPlaying: onFinishedPlaying,
@@ -183,46 +167,48 @@ const Playback = ({
switch (state.view) {
case PRELOAD:
return (
-
{
- setView(playerType);
+ {
+ setView(playbackArgs.view);
onPreloadReady();
}}
/>
);
case AUTOPLAY:
- return ;
+ return ;
case BUTTON:
return (
-1}
- disabled={playConfig.play_once && hasPlayed.includes(0)}
+ disabled={playbackArgs.play_once && hasPlayed.includes(0)}
/>
);
case MULTIPLAYER:
return (
);
- case SPECTROGRAM:
+ case IMAGE:
return (
-
);
case MATCHINGPAIRS:
return (
);
case VISUALMATCHINGPAIRS:
@@ -238,7 +224,7 @@ const Playback = ({
return (
-
{render(playerType)}
{" "}
+
{render(playbackArgs.view)}
{" "}
);
};
diff --git a/frontend/src/components/Playback/Playback.test.js b/frontend/src/components/Playback/Playback.test.js
new file mode 100644
index 000000000..52a56aa72
--- /dev/null
+++ b/frontend/src/components/Playback/Playback.test.js
@@ -0,0 +1,43 @@
+import React from 'react';
+import { render, wait } from '@testing-library/react';
+
+import Playback from './Playback';
+
+describe('Playback', () => {
+
+ const basicProps = {
+ autoAdvance: false,
+ responseTime: 42,
+ onPreloadReady: jest.fn(),
+ startedPlaying: jest.fn(),
+ finishedPlaying: jest.fn(),
+ submitResult: jest.fn(),
+ }
+
+ let playbackArgs = {
+ view: 'BUTTON',
+ show_animation: false,
+ instruction: 'Listen, just listen!',
+ play_method: 'HTML',
+ ready_time: 1,
+ preload_message: 'Get ready',
+ sections: [{id: 13, url: 'some/fancy/tune.mp3'}]
+ };
+
+ it('renders itself', () => {
+ const { container } = render(
+ );
+ expect(container.querySelector('.aha__playback')).toBeInTheDocument();
+ });
+
+ it('shows Preload during ready_time', () => {
+ const { container } = render(
+ );
+ expect(container.querySelector('.aha__listen')).toBeInTheDocument();
+ });
+
+})
\ No newline at end of file
diff --git a/frontend/src/components/Preload/Preload.js b/frontend/src/components/Preload/Preload.js
index e65cb765d..eb3fa258b 100644
--- a/frontend/src/components/Preload/Preload.js
+++ b/frontend/src/components/Preload/Preload.js
@@ -1,41 +1,41 @@
-import React, { useEffect, useRef, useState } from "react";
+import React, { useEffect, useState } from "react";
+import classNames from "classnames";
+import { MEDIA_ROOT } from "../../config";
import ListenFeedback from "../Listen/ListenFeedback";
import CountDown from "../CountDown/CountDown";
import * as audio from "../../util/audio";
import * as webAudio from "../../util/webAudio";
-import { MEDIA_ROOT } from "../../config";
-import classNames from "classnames";
// Preload is an experiment screen that continues after a given time or after an audio file has been preloaded
-const Preload = ({ instruction, pageTitle, duration, sections, playConfig, onNext }) => {
- const timeHasPassed = useRef(false);
- const audioIsAvailable = useRef(false);
- const [loaderDuration, setLoaderDuration] = useState(duration);
+const Preload = ({ sections, playMethod, duration, preloadMessage, pageTitle, onNext }) => {
+ const [timePassed, setTimePassed] = useState(false);
+ const [audioAvailable, setAudioAvailable] = useState(false);
const [overtime, setOvertime] = useState(false);
-
+ const [loaderDuration, setLoaderDuration] = useState(duration);
+
const onTimePassed = () => {
- timeHasPassed.current = true;
+ setTimePassed(true)
setLoaderDuration(0);
setOvertime(true);
- if (audioIsAvailable.current) {
+ if (audioAvailable) {
onNext();
}
};
+ // Audio preloader
useEffect(() => {
const preloadResources = async () => {
-
- if (playConfig.play_method === 'PREFETCH') {
+ if (playMethod === 'NOAUDIO') {
await Promise.all(sections.map((section) => fetch(MEDIA_ROOT + section.url)));
return onNext();
}
- if (playConfig.play_method === 'BUFFER') {
+ if (playMethod === 'BUFFER') {
- // Use Web-audio and preload sections in buffers
+ // Use Web-audio and preload sections in buffers
sections.map((section, index) => {
// skip Preload if the section has already been loaded in the previous action
if (webAudio.checkSectionLoaded(section)) {
@@ -47,40 +47,40 @@ const Preload = ({ instruction, pageTitle, duration, sections, playConfig, onNex
webAudio.clearBuffers();
}
- // Load sections in buffer
- return webAudio.loadBuffer(section.id, section.url, () => {
+ // Load sections in buffer
+ return webAudio.loadBuffer(section.id, section.url, () => {
if (index === (sections.length - 1)) {
- audioIsAvailable.current = true;
- if (timeHasPassed.current) {
+ setAudioAvailable(true);
+ if (timePassed) {
onNext();
- }
- }
+ }
+ }
});
})
} else {
- if (playConfig.play_method === 'EXTERNAL') {
- webAudio.closeWebAudio();
+ if (playMethod === 'EXTERNAL') {
+ webAudio.closeWebAudio();
}
// Load audio until available
- // Return remove listener
- return audio.loadUntilAvailable(MEDIA_ROOT + sections[0].url, () => {
- audioIsAvailable.current = true;
- if (timeHasPassed.current) {
+ // Return remove listener
+ return audio.loadUntilAvailable(sections[0].url, () => {
+ setAudioAvailable(true);
+ if (timePassed) {
onNext();
}
- });
+ });
}
}
- preloadResources();
- }, [sections, onNext, playConfig]);
-
+ preloadResources();
+ }, [sections, playMethod, onNext, timePassed]);
+
return (
= 1 && }
/>
diff --git a/frontend/src/components/Trial/Trial.js b/frontend/src/components/Trial/Trial.js
index c04759a2b..6ba2b74ed 100644
--- a/frontend/src/components/Trial/Trial.js
+++ b/frontend/src/components/Trial/Trial.js
@@ -23,7 +23,7 @@ const Trial = ({
}) => {
// Main component state
const [formActive, setFormActive] = useState(!config.listen_first);
- const [preloadReady, setPreloadReady] = useState(!playback?.play_config?.ready_time);
+ const [preloadReady, setPreloadReady] = useState(!(playback?.ready_time));
const submitted = useRef(false);
@@ -81,7 +81,7 @@ const Trial = ({
}
},
- [feedback_form, config, onNext, onResult]
+ [feedback_form, config, onNext, onResult, result_id]
);
const checkBreakRound = (values, breakConditions) => {
@@ -108,8 +108,8 @@ const Trial = ({
if (config.auto_advance) {
// Create a time_passed result
- if (config.auto_advance_timer != null) {
- if (playback.player_type === 'BUTTON') {
+ if (config.auto_advance_timer != null) {
+ if (playback.view === 'BUTTON') {
startTime.current = getCurrentTime();
}
@@ -131,16 +131,12 @@ const Trial = ({
{playback && (
{
setPreloadReady(true);
}}
- preloadMessage={playback.preload_message}
autoAdvance={config.auto_advance}
responseTime={config.response_time}
- playConfig={playback.play_config}
- sections={playback.sections}
submitResult={makeResult}
startedPlaying={startTimer}
finishedPlaying={finishedPlaying}
diff --git a/frontend/src/components/components.scss b/frontend/src/components/components.scss
index 393489d5b..1100b623a 100644
--- a/frontend/src/components/components.scss
+++ b/frontend/src/components/components.scss
@@ -34,7 +34,7 @@
@import "./Playback/Multiplayer";
@import "./Playback/Playback";
@import "./Playback/MatchingPairs";
-@import "./Playback/SpectrogramPlayer";
+@import "./Playback/ImagePlayer";
@import "./Question/Question";
@import "./Score/Score";
@import "./Trial/Trial";
diff --git a/frontend/src/util/audio.js b/frontend/src/util/audio.js
index 72ffb8f6b..271f35fbe 100644
--- a/frontend/src/util/audio.js
+++ b/frontend/src/util/audio.js
@@ -1,4 +1,4 @@
-import { API_ROOT, SILENT_MP3 } from "../config.js";
+import { API_ROOT, MEDIA_ROOT, SILENT_MP3 } from "../config.js";
import Timer from "./timer";
// Audio provides function around a shared audio object
@@ -170,7 +170,7 @@ export const loadUntilAvailable = (src, canPlay) => {
// without having to stop for further buffering of content.
const removeListener = listenOnce("canplaythrough", canPlay);
- load(src);
+ load(MEDIA_ROOT + src);
// If the ready state is already > 3, data is already loaded;
// Call canPlay right away
diff --git a/frontend/src/util/audioControl.js b/frontend/src/util/audioControl.js
index 0e8b61458..ce1e8ed4a 100644
--- a/frontend/src/util/audioControl.js
+++ b/frontend/src/util/audioControl.js
@@ -1,22 +1,21 @@
import * as audio from "./audio";
import * as webAudio from "./webAudio";
-export const playAudio = (playConfig, section, playheadShift=0) => {
+export const playAudio = (section, playMethod, playheadShift=0) => {
let latency = 0;
- const playhead = playConfig.playhead + playheadShift
- if (playConfig.play_method === 'BUFFER') {
+ if (playMethod === 'BUFFER') {
// Determine latency for current audio device
latency = webAudio.getTotalLatency()
// Play audio
- webAudio.playBufferFrom(section.id, playhead);
+ webAudio.playBufferFrom(section.id, playheadShift);
return latency
} else {
// Only initialize webaudio if section is hosted local
- if (playConfig.play_method !== 'EXTERNAL') {
+ if (playMethod !== 'EXTERNAL') {
// Determine latency for current audio device
latency = webAudio.getTotalLatency()
webAudio.initWebAudio();
@@ -26,14 +25,14 @@ export const playAudio = (playConfig, section, playheadShift=0) => {
audio.setVolume(1);
// Play audio
- audio.playFrom(Math.max(0, playhead));
+ audio.playFrom(Math.max(0, playheadShift));
return latency
}
}
-export const pauseAudio = (playConfig) => {
- if (playConfig.play_method === 'BUFFER') {
+export const pauseAudio = (playMethod) => {
+ if (playMethod === 'BUFFER') {
webAudio.stopBuffer();
} else {
audio.stop();
diff --git a/frontend/src/util/label.js b/frontend/src/util/label.js
index eec6271fb..86ea9cf3b 100644
--- a/frontend/src/util/label.js
+++ b/frontend/src/util/label.js
@@ -1,32 +1,3 @@
-import { romanNumeral } from "./roman";
-
-export const LABEL_NUMERIC = "NUMERIC";
-export const LABEL_ALPHABETIC = "ALPHABETIC";
-export const LABEL_CUSTOM = "CUSTOM";
-export const LABEL_ROMAN = "ROMAN";
-
-/**
- * @deprecated This function is deprecated and will be removed in the future.
- * See also https://github.com/Amsterdam-Music-Lab/MUSCLE/pull/640
- * Get a player label, based on index, labelstyle and customLabels
- */
-export const getPlayerLabel = (index, labelStyle, customLabels) => {
- index = parseInt(index);
-
- switch (labelStyle) {
- case LABEL_NUMERIC:
- return parseInt(index) + 1;
- case LABEL_ALPHABETIC:
- return String.fromCharCode(65 + index);
- case LABEL_ROMAN:
- return romanNumeral(index + 1);
- case LABEL_CUSTOM:
- return customLabels[index] || "";
- default:
- return "";
- }
-};
-
export const renderLabel = (label, size="fa-lg") => {
if (!label) return label
if (label.startsWith('fa-')) return
diff --git a/frontend/src/util/label.test.js b/frontend/src/util/label.test.js
index e27736501..b97f434d7 100644
--- a/frontend/src/util/label.test.js
+++ b/frontend/src/util/label.test.js
@@ -1,35 +1,5 @@
import { render } from '@testing-library/react';
-import { getPlayerLabel, LABEL_NUMERIC, LABEL_ALPHABETIC, LABEL_ROMAN, LABEL_CUSTOM, renderLabel } from "./label";
-
-describe('getPlayerLabel', () => {
-
- it('returns numeric label correctly', () => {
- expect(getPlayerLabel(0, LABEL_NUMERIC)).toBe(1);
- expect(getPlayerLabel(1, LABEL_NUMERIC)).toBe(2);
- });
-
- it('returns alphabetic label correctly', () => {
- expect(getPlayerLabel(0, LABEL_ALPHABETIC)).toBe('A');
- expect(getPlayerLabel(25, LABEL_ALPHABETIC)).toBe('Z');
- });
-
- it('returns roman label correctly', () => {
- expect(getPlayerLabel(0, LABEL_ROMAN)).toBe('I');
- expect(getPlayerLabel(3, LABEL_ROMAN)).toBe('IV');
- });
-
- it('returns custom label correctly', () => {
- const customLabels = ['One', 'Two', 'Three'];
- expect(getPlayerLabel(0, LABEL_CUSTOM, customLabels)).toBe('One');
- expect(getPlayerLabel(2, LABEL_CUSTOM, customLabels)).toBe('Three');
- });
-
- it('returns empty string for unknown label style', () => {
- expect(getPlayerLabel(1, 'UNKNOWN')).toBe('');
- });
-
-});
-
+import { renderLabel } from "./label";
describe('renderLabel', () => {
diff --git a/frontend/src/util/roman.js b/frontend/src/util/roman.js
deleted file mode 100644
index 6663a95eb..000000000
--- a/frontend/src/util/roman.js
+++ /dev/null
@@ -1,21 +0,0 @@
-export const romanNumeral = (int) => {
- let roman = '';
-
- if (int < 0 || !int) return roman;
-
- roman += 'M'.repeat(int / 1000); int %= 1000;
- roman += 'CM'.repeat(int / 900); int %= 900;
- roman += 'D'.repeat(int / 500); int %= 500;
- roman += 'CD'.repeat(int / 400); int %= 400;
- roman += 'C'.repeat(int / 100); int %= 100;
- roman += 'XC'.repeat(int / 90); int %= 90;
- roman += 'L'.repeat(int / 50); int %= 50;
- roman += 'XL'.repeat(int / 40); int %= 40;
- roman += 'X'.repeat(int / 10); int %= 10;
- roman += 'IX'.repeat(int / 9); int %= 9;
- roman += 'V'.repeat(int / 5); int %= 5;
- roman += 'IV'.repeat(int / 4); int %= 4;
- roman += 'I'.repeat(int);
-
- return roman;
-}
diff --git a/frontend/src/util/roman.test.js b/frontend/src/util/roman.test.js
deleted file mode 100644
index 322b6ffe2..000000000
--- a/frontend/src/util/roman.test.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import { romanNumeral } from './roman';
-
-describe('romanNumeral', () => {
- it('converts basic numbers correctly', () => {
- expect(romanNumeral(1)).toBe('I');
- expect(romanNumeral(5)).toBe('V');
- expect(romanNumeral(10)).toBe('X');
- expect(romanNumeral(50)).toBe('L');
- expect(romanNumeral(100)).toBe('C');
- expect(romanNumeral(500)).toBe('D');
- expect(romanNumeral(1000)).toBe('M');
- });
-
- it('converts composite numbers correctly', () => {
- expect(romanNumeral(23)).toBe('XXIII');
- expect(romanNumeral(44)).toBe('XLIV');
- expect(romanNumeral(89)).toBe('LXXXIX');
- expect(romanNumeral(199)).toBe('CXCIX');
- expect(romanNumeral(499)).toBe('CDXCIX');
- });
-
- it('handles subtractive notation correctly', () => {
- expect(romanNumeral(4)).toBe('IV');
- expect(romanNumeral(9)).toBe('IX');
- expect(romanNumeral(40)).toBe('XL');
- expect(romanNumeral(90)).toBe('XC');
- expect(romanNumeral(400)).toBe('CD');
- expect(romanNumeral(900)).toBe('CM');
- expect(romanNumeral(444)).toBe('CDXLIV');
- expect(romanNumeral(999)).toBe('CMXCIX');
- });
-
- it('converts large numbers correctly', () => {
- expect(romanNumeral(1984)).toBe('MCMLXXXIV');
- expect(romanNumeral(2022)).toBe('MMXXII');
- expect(romanNumeral(3999)).toBe('MMMCMXCIX');
- expect(romanNumeral(4444)).toBe('MMMMCDXLIV');
- expect(romanNumeral(9999)).toBe('MMMMMMMMMCMXCIX');
- });
-
- it('handles edge cases correctly', () => {
- expect(romanNumeral(0)).toBe('');
- expect(romanNumeral(-1)).toBe('');
- });
-});
diff --git a/frontend/src/util/webAudio.js b/frontend/src/util/webAudio.js
index d34450b3e..a113a3ec7 100644
--- a/frontend/src/util/webAudio.js
+++ b/frontend/src/util/webAudio.js
@@ -105,14 +105,6 @@ export const stopBuffer = () => {
}
}
-// play buffer by section.id
-export const playBuffer = (id) => {
- source = audioContext.createBufferSource();
- source.buffer = buffers[id];
- source.connect(audioContext.destination);
- source.start();
-}
-
// Play buffer from given time
export const playBufferFrom = (id, time) => {
source = audioContext.createBufferSource();