Skip to content

Commit

Permalink
Playback and mobile fixes (#18)
Browse files Browse the repository at this point in the history
    Request microphone permission when loading page. App now supports chrome and Mobile!.
    Improve reducer action names.
    Remove autoplay from record button and into higher level component
    Fix issue with Quiz typing id property.
    Remove unused reducer actions
    Remove currentuizID concept and instead use quiz[0] always. Will need this for upcoming "continuous mode" lessons that never end.
    Remove a hack in the UI during lesson download
    Disable record button if grading is in progress (might not need this later, could remove)
    Add some missing return / await statements in the audio recorder code.
  • Loading branch information
RickCarlino authored Sep 25, 2023
1 parent e03e029 commit cdb69f6
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 127 deletions.
17 changes: 4 additions & 13 deletions components/play-button.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { Button } from "@mantine/core";
import { useHotkeys } from "@mantine/hooks";
import { useEffect } from "react";

let audioContext: AudioContext;
// let audioQueue: string[] = [];
let currentlyPlaying = false;

const playAudioBuffer = (buffer: AudioBuffer): Promise<void> => {
Expand All @@ -22,11 +20,11 @@ const playAudioBuffer = (buffer: AudioBuffer): Promise<void> => {
export const playAudio = (urlOrDataURI: string): Promise<void> => {
return new Promise((resolve, reject) => {
if (!urlOrDataURI) {
resolve();
return resolve();
}

if (currentlyPlaying) {
return;
return resolve();
}

currentlyPlaying = true;
Expand Down Expand Up @@ -61,20 +59,13 @@ export const playAudio = (urlOrDataURI: string): Promise<void> => {

/** A React component */
export function PlayButton({ dataURI }: { dataURI: string }) {
const playSound = () => {
const playSound = async () => {
if (dataURI) {
playAudio(dataURI);
await playAudio(dataURI);
}
};
useHotkeys([["c", playSound]]);

// Use the useEffect hook to listen for changes to dataURI
useEffect(() => {
if (dataURI) {
playAudio(dataURI);
}
}, [dataURI]); // Dependency array includes dataURI

if (!dataURI) {
return (
<>
Expand Down
13 changes: 8 additions & 5 deletions components/record-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ type Props = {
onStart?: () => void;
onRecord: (data: string) => void;
lessonType: string;
disabled?: boolean;
};
export function RecordButton(props: Props) {
const { isRecording, stop, start } = useVoiceRecorder(async (data) => {
Expand All @@ -114,9 +115,11 @@ export function RecordButton(props: Props) {
props.onRecord(b64);
});
const doStart = () => {
props.onStart?.();
start();
}
if (!props.disabled) {
props.onStart?.();
start();
}
};
useHotkeys([
[
"v",
Expand All @@ -138,11 +141,11 @@ export function RecordButton(props: Props) {
break;
}
return isRecording ? (
<Button onClick={stop} color="red" fullWidth>
<Button onClick={stop} color="red" fullWidth disabled={props.disabled}>
[V]⏹️Submit Answer
</Button>
) : (
<Button onClick={start} fullWidth>
<Button onClick={start} fullWidth disabled={props.disabled}>
[V]⏺️{buttonText}
</Button>
);
Expand Down
97 changes: 31 additions & 66 deletions pages/_study_reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ export type Quiz = {

type State = {
numQuizzesAwaitingServerResponse: number;
currentQuizIndex: number;
errors: string[];
quizIDsForLesson: string[];
failedQuizzes: Set<string>;
quizIDsForLesson: number[];
phrasesById: Record<string, Quiz>;
};

Expand All @@ -24,11 +22,10 @@ type LessonType = keyof Quiz["audio"];
type QuizResult = "error" | "failure" | "success";

type Action =
| { type: "WILL_GRADE" }
| { type: "ADD_ERROR"; message: string }
| { type: "FAIL_QUIZ"; id: string }
| { type: "FLAG_QUIZ"; id: string }
| { type: "DID_GRADE"; id: string; result: QuizResult };
| { type: "WILL_GRADE"; id: number }
| { type: "USER_GAVE_UP"; id: number }
| { type: "FLAG_QUIZ"; id: number }
| { type: "DID_GRADE"; id: number; result: QuizResult };

export type CurrentQuiz = {
id: number;
Expand All @@ -39,29 +36,26 @@ export type CurrentQuiz = {
repetitions: number;
};

export function gotoNextQuiz(state: State): State {
return {
...state,
currentQuizIndex: state.currentQuizIndex + 1
};
export function gotoNextQuiz(state: State, lastQuizID: number): State {
const filter = (id: number) => id !== lastQuizID;
const quizIDsForLesson = state.quizIDsForLesson.filter(filter);
return { ...state, quizIDsForLesson };
}

export const newQuizState = (state: Partial<State> = {}): State => {
const phrasesById = state.phrasesById || {};
const remainingQuizIDs = Object.keys(phrasesById);
const remainingQuizIDs = Object.keys(phrasesById).map((x) => parseInt(x));
return {
currentQuizIndex: 0,
numQuizzesAwaitingServerResponse: 0,
phrasesById,
quizIDsForLesson: remainingQuizIDs,
errors: [],
failedQuizzes: new Set(),
...state,
};
};

export function currentQuiz(state: State): CurrentQuiz | undefined {
const quizID = state.quizIDsForLesson[state.currentQuizIndex];
const quizID = state.quizIDsForLesson[0];
const quiz = state.phrasesById[quizID];
if (!quiz) {
return undefined;
Expand All @@ -74,7 +68,9 @@ export function currentQuiz(state: State): CurrentQuiz | undefined {
if (quiz.repetitions < 2) {
lessonType = "dictation";
} else {
lessonType = Math.random() > 0.5 ? "listening" : "speaking";
const nonce = quiz.id + quiz.repetitions;
const x = nonce % 2;
lessonType = x === 0 ? "listening" : "speaking";
}
return (
quiz && {
Expand All @@ -88,67 +84,36 @@ export function currentQuiz(state: State): CurrentQuiz | undefined {
);
}

export function quizReducer(state: State, action: Action): State {
function reduce(state: State, action: Action): State {
switch (action.type) {
case "ADD_ERROR":
return { ...state, errors: [...state.errors, action.message] };

case "FAIL_QUIZ":
// Add to failed quiz set.
const failedQuizzes = new Set(state.failedQuizzes);
failedQuizzes.add(action.id);
// Remove from list of remaining quizzes.
const remainingQuizIDs = state.quizIDsForLesson.filter(
(id) => id !== action.id,
);
return gotoNextQuiz({
...state,
failedQuizzes,
quizIDsForLesson: remainingQuizIDs,
});

case "USER_GAVE_UP":
case "FLAG_QUIZ":
const filter = (id: string) => id !== action.id;
return gotoNextQuiz({
...state,
quizIDsForLesson: state.quizIDsForLesson.filter(filter),
});

return gotoNextQuiz(state, action.id);
case "WILL_GRADE":
return {
...state,
numQuizzesAwaitingServerResponse:
state.numQuizzesAwaitingServerResponse + 1,
};

case "DID_GRADE":
let numQuizzesAwaitingServerResponse =
state.numQuizzesAwaitingServerResponse - 1;
const nextState = {
...state,
numQuizzesAwaitingServerResponse,
};
switch (action.result) {
case "failure":
return gotoNextQuiz({
...nextState,
failedQuizzes: new Set(state.failedQuizzes).add(action.id),
});
case "error":
// In the case of a server error,
// we push the quiz onto the end of the list
// and try again later.
return gotoNextQuiz({
...nextState,
quizIDsForLesson: [...state.quizIDsForLesson, action.id],
});
case "success":
return gotoNextQuiz({ ...nextState });
default:
throw new Error("Invalid quiz result " + action.result);
}
return gotoNextQuiz(
{
...state,
numQuizzesAwaitingServerResponse,
},
action.id,
);
default:
console.warn("Unhandled action", action);
return state;
}
}

export function quizReducer(state: State, action: Action): State {
const nextState = reduce(state, action);
// Do debugging here:
// console.log(action.type);
return nextState;
}
Loading

0 comments on commit cdb69f6

Please sign in to comment.