Created
January 14, 2021 07:01
-
-
Save aeschylus/dcf750b5c6f08b1b181b120e5a4dc253 to your computer and use it in GitHub Desktop.
Generated by XState Viz: https://xstate.js.org/viz
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// config | |
var clip; // For audioSource | |
const numListeningLoops = 5; | |
const numPrimingLoops = 2; | |
const numRecordingReps = 2; | |
const numComparisonLoops = 2; | |
const partiallyTimedStudyMachine = Machine({ | |
initial: 'loading', | |
context: { | |
listeningLoopsRemaining: numListeningLoops, | |
primingLoopsRemaining: numPrimingLoops, | |
recordingRepsRemaining: numRecordingReps, | |
comparisonLoopsRemaining: numComparisonLoops, | |
currentPlaytime: 0, | |
duration: null, | |
clip: null, | |
mediaRecorder: null, | |
lastRecording: null, | |
}, | |
states: { | |
loading: { | |
on: { | |
'CLIP_LOADED': { | |
target: 'listening', | |
actions: 'setAudioResources' | |
} | |
} | |
}, | |
listening: { | |
initial: 'looping', | |
onDone: { | |
target: 'practicing', | |
actions: [ | |
'resetPlaytime', | |
'resetListeningLoopsRemaining' | |
] | |
}, | |
states: { | |
looping: { | |
entry: 'playAudio', | |
on: { | |
FINISH_PLAYBACK: [{ | |
target: 'paused', | |
actions: [ | |
'resetPlaytime' | |
], | |
cond: 'listeningLoopsCompleted' | |
}, | |
{ | |
target:'looping', | |
actions: [ | |
'updateListeningLoopsRemaining', | |
'resetPlaytime' | |
] | |
}], | |
TICK: { | |
actions: 'updatePlaytime' | |
} | |
} | |
}, | |
paused: { | |
entry: 'resetListeningLoopsRemaining', | |
on: { | |
LISTEN_AGAIN: 'looping', | |
CONTINUE_TO_RECORDING: 'done' | |
} | |
}, | |
done: { | |
type: 'final' | |
} | |
} | |
}, | |
practicing: { | |
initial: 'recording', | |
onDone: [{ | |
target: 'done', | |
cond: 'comparisonLoopsCompleted', | |
actions: 'resetComparisonLoopsRemaining' | |
}, | |
{ | |
target: 'practicing', | |
actions: 'updateComparisonLoopsRemaining' | |
}], | |
states: { | |
recording: { | |
initial: 'priming', | |
onDone: [{ | |
target: 'reviewing', | |
cond: 'recordingRepsMet', | |
actions: 'resetRecordingRepsRemaining' | |
}, | |
{ | |
target:'recording', | |
actions: 'updateRecordingRepsRemaining' | |
}], | |
states: { | |
priming: { | |
initial: 'loopingOriginal', | |
onDone: { | |
target: 'recording', | |
actions: 'resetPrimingLoopsRemaining' | |
}, | |
states: { | |
loopingOriginal:{ | |
invoke: { | |
src: 'audioClipService' | |
}, | |
on: { | |
FINISH_PLAYBACK: [{ | |
target: 'done', | |
actions: 'resetPlaytime', | |
cond: 'primingLoopsCompleted' | |
}, | |
{ | |
target:'loopingOriginal', | |
actions: 'updatePrimingLoopsRemaining' | |
}], | |
TICK: { | |
actions: 'updatePlaytime' | |
} | |
} | |
}, | |
done: { | |
type:'final' | |
} | |
} | |
}, | |
recording: { | |
initial: 'settingUpMicrophone', | |
onDone: 'done', | |
states: { | |
settingUpMicrophone: { | |
invoke: { | |
src: 'microphoneService' | |
}, | |
on: { | |
RECORDER_INITIALISED: { | |
target: 'recording', | |
actions: assign({ | |
mediaRecorder: (context, event) => event.mediaRecorder | |
}) | |
} | |
} | |
}, | |
recording: { | |
invoke: [ | |
{ | |
id: 'timerService', | |
src: 'timer' | |
}, | |
{ | |
id: 'recordingSession', | |
src: 'recordingSession' | |
} | |
], | |
on: { | |
SAVE_RECORDING: { | |
target: 'done', | |
actions: 'saveRecording' | |
}, | |
STOP_TIMER: { | |
actions: 'stopRecording' | |
} | |
} | |
}, | |
done: { | |
type: 'final' | |
} | |
}, | |
done: { | |
type: 'final' | |
} | |
}, | |
done: { | |
type: 'final' | |
} | |
} | |
}, | |
reviewing: { | |
initial: 'comparing', | |
onDone: 'done', | |
states: { | |
comparing: { | |
initial: 'playingOriginal', | |
onDone: { | |
target: 'evaluating', | |
//actions: 'resetComparisonReps' | |
}, | |
states: { | |
playingOriginal: { | |
invoke: { | |
src: 'audioClipService' | |
}, | |
on: { | |
FINISH_PLAYBACK: { | |
target: 'playingRecording', | |
actions: 'resetPlaytime' | |
}, | |
TICK: { | |
actions: 'updatePlaytime' | |
} | |
} | |
}, | |
playingRecording:{ | |
invoke: { | |
src: 'recordingPlaybackService' | |
}, | |
on: { | |
FINISH_PLAYBACK: { | |
target: 'done', | |
actions: 'resetPlaytime' | |
}, | |
TICK: { | |
actions: 'updatePlaytime' | |
} | |
} | |
}, | |
done: { | |
type:'final' | |
} | |
} | |
}, | |
evaluating: { | |
on: { | |
APPROVE: 'done', | |
REPLAY_COMPARISON: 'comparing', | |
REPLAY_ONLY_RECORDING: 'comparing.playingRecording' | |
} | |
}, | |
done: { | |
type: 'final' | |
} | |
} | |
}, | |
done: { | |
type: 'final' | |
} | |
} | |
}, | |
done: { | |
type: 'final' | |
} | |
} | |
}, | |
{ | |
guards: { | |
listeningLoopsCompleted: (context, event) => { | |
return context.listeningLoopsRemaining === 1; | |
}, | |
primingLoopsCompleted: (context, event) => { | |
return context.primingLoopsRemaining === 1; | |
}, | |
recordingRepsMet: (context, event) => { | |
return context.recordingRepsRemaining === 1; | |
}, | |
comparisonLoopsCompleted: (context, event) => { | |
return context.comparisonLoopsRemaining === 1; | |
} | |
}, | |
actions: { | |
// setAudioResources: assign({ | |
// audioCtx: (context, event) => event.audioCtx, | |
// clipNode: (context, event) => event.clipNode | |
//}), | |
setAudioResources: assign({ | |
duration: (context, event) => { | |
console.log('called'); | |
console.log(event); | |
return event.clip.duration | |
}, | |
clip: (context, event) => event.clip | |
}), | |
playAudio: (context, event) => { | |
context.clip.play(); | |
}, | |
updatePlaytime: assign({ | |
currentPlaytime: (context, event) => event.newTime | |
}), | |
resetPlaytime: assign({ | |
currentPlaytime: (context, event) => 0 | |
}), | |
updateListeningLoopsRemaining: assign({ | |
listeningLoopsRemaining: context => context.listeningLoopsRemaining - 1 | |
}), | |
resetListeningLoopsRemaining: assign({ | |
listeningLoopsRemaining: context => numListeningLoops | |
}), | |
updatePrimingLoopsRemaining: assign({ | |
primingLoopsRemaining: context => context.primingLoopsRemaining - 1 | |
}), | |
resetPrimingLoopsRemaining: assign({ | |
primingLoopsRemaining: context => numPrimingLoops | |
}), | |
updateRecordingRepsRemaining: assign({ | |
recordingRepsRemaining: context => context.recordingRepsRemaining - 1 | |
}), | |
resetRecordingRepsRemaining: assign({ | |
recordingRepsRemaining: context => numRecordingReps | |
}), | |
updateComparisonLoopsRemaining: assign({ | |
comparisonLoopsRemaining: context => context.comparisonLoopsRemaining - 1 | |
}), | |
resetComparisonLoopsRemaining: assign({ | |
comparisonLoopsRemaining: context => numComparisonLoops | |
}), | |
saveRecording: assign((context, event) => { | |
const blob = new Blob([event.bufferChunk], { 'type' : 'audio/ogg; codecs=opus' }); | |
const newRecording = new Audio(window.URL.createObjectURL(blob)) | |
return { | |
lastRecording: newRecording | |
} | |
}), | |
stopRecording: (context, event) => { | |
context.mediaRecorder.stop() | |
} | |
}, | |
services: { | |
recordingPlaybackService: context => cb => { | |
const lastRecording = context.lastRecording; | |
lastRecording.addEventListener('timeupdate', event => { | |
cb({ | |
type: 'TICK', | |
newTime: lastRecording.currentTime | |
}) | |
}); | |
lastRecording.addEventListener('ended', event => { | |
lastRecording.currentTime = 0; | |
cb('FINISH_PLAYBACK') | |
}); | |
lastRecording.play(); | |
return () => { | |
} | |
}, | |
microphoneService: context => cb => { | |
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { | |
console.log('getUserMedia supported.'); | |
navigator.mediaDevices.getUserMedia ( | |
// constraints - only audio needed for this app | |
{ | |
audio: true | |
}) | |
// Success callback | |
.then(function(stream) { | |
const mediaRecorder = new MediaRecorder(stream); | |
cb({ | |
type: 'RECORDER_INITIALISED', | |
mediaRecorder: mediaRecorder | |
}) | |
}) | |
// Error callback | |
.catch(function(err) { | |
console.log('The following getUserMedia error occured: ' + err); | |
}); | |
} else { | |
console.log('getUserMedia not supported on your browser!'); | |
} | |
return () => {}; | |
}, | |
timer: context => cb => { | |
setTimeout(()=>{ | |
cb('STOP_TIMER') | |
// }, clip.duration * 1000 + 700); | |
}, 1000 + 700); | |
}, | |
recordingSession: context => cb => { | |
context.mediaRecorder.ondataavailable = function(e) { | |
console.log('new data chunk available'); | |
cb({ | |
type: 'SAVE_RECORDING', | |
bufferChunk: e.data | |
}); | |
}; | |
context.mediaRecorder.start(); | |
return () => {} | |
} | |
} | |
}); | |
window.service = interpret(partiallyTimedStudyMachine) | |
.onTransition(state => { | |
if (state.changed) { | |
console.log(state.value); | |
} | |
}) | |
.start(); // returns started service | |
const setupAudio = () => { | |
clip = new Audio('https://audio.tatoeba.org/sentences/spa/2711500.mp3'); | |
clip.addEventListener('canplaythrough', (event) => { | |
service.send({ | |
type: 'CLIP_LOADED', | |
clip: clip | |
}) | |
}); | |
clip.addEventListener('ended', (event) => { | |
service.send({ | |
type: 'FINISH_PLAYBACK' | |
}) | |
}); | |
clip.addEventListener('timeupdate', (event) => { | |
service.send({ | |
type: 'TICK', | |
clip: clip | |
}) | |
}); | |
clip.addEventListener('pause', ()=>console.log('paused')); | |
clip.addEventListener('waiting', ()=>console.log('waiting')); | |
clip.addEventListener('play', ()=>console.log('play')); | |
clip.addEventListener('playing', ()=>console.log('"playing" - (ready to play(????))')); | |
clip.addEventListener('stalled', ()=>console.log('stalled')); | |
} | |
setupAudio(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment