Skip to content

Instantly share code, notes, and snippets.

@aeschylus
Last active September 22, 2020 20:02
Show Gist options
  • Save aeschylus/c04ea86b2ea3f765a61063aaa7163c94 to your computer and use it in GitHub Desktop.
Save aeschylus/c04ea86b2ea3f765a61063aaa7163c94 to your computer and use it in GitHub Desktop.
Generated by XState Viz: https://xstate.js.org/viz
// 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