Skip to content

Commit 094226a

Browse files
authored
Robert spring interface (#182)
* Changing layout - first draft * Interface is nicer-looking now * It stops when the stop button is pushed - and the cursor moves * The cursor only moves if the audio recorder is commented out?? * Try putting microphone and all synchronization requests together * It worked! My literal last idea worked * Commented functioning version in anticipation of pull request --------- Co-authored-by: RMRattray <rmr3141@gmail.com>
1 parent 0a484b1 commit 094226a

File tree

11 files changed

+464
-145
lines changed

11 files changed

+464
-145
lines changed

frontend/companion-app/App.tsx

Lines changed: 153 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
// Import necessary modules and components from the Expo and React Native libraries
22
import { StatusBar } from "expo-status-bar";
3-
import { StyleSheet, Text, View, SafeAreaView } from "react-native";
4-
import React, { useEffect, useReducer } from "react";
3+
import { StyleSheet, Text, View, Image, SafeAreaView } from "react-native";
4+
import React, { useEffect, useReducer, useRef, useState } from "react";
55
import { startSession, synchronize } from "./components/Utils";
66
import { Score_Select } from "./components/ScoreSelect";
7+
import { Return_Button } from "./components/ReturnButton";
78
import { Start_Stop_Button } from "./components/StartButton";
89
import { MeasureSetBox } from "./components/MeasureSetter";
910
import { TempoBox } from "./components/TempoBox";
1011
import { Fraction } from "opensheetmusicdisplay";
11-
import AudioRecorder from "./components/AudioRecorder";
12+
import AudioRecorder from "./components/AudioRecorderStateVersion";
1213
import { AudioPlayer } from "./components/AudioPlayer";
1314
import reducer_function from "./Dispatch";
1415
import ScoreDisplay from "./components/ScoreDisplay";
@@ -28,6 +29,7 @@ export default function App() {
2829
const [state, dispatch] = useReducer(
2930
reducer_function, // The reducer function is found in Dispatch.ts
3031
{
32+
inPlayMode: false, // whether we are in play mode (and not score selection mode)
3133
playing: false, // whether the audio is playing
3234
resetMeasure: 1, // the measure to reset to
3335
playRate: 1.0, // the rate at which the audio is playing
@@ -40,7 +42,7 @@ export default function App() {
4042
synth_tempo: 100, // the tempo of the synthesized audio
4143
tempo: 100, // the tempo in the tempo box (even if changed more recently)
4244
score_tempo: 100, // the tempo in the musical score
43-
scores: [], // the list of scores to choose from
45+
scores: [] // the list of scores to choose from
4446
},
4547
);
4648

@@ -61,83 +63,163 @@ export default function App() {
6163
fetchSessionToken();
6264
}, []);
6365

64-
console.log(state);
66+
////////////////////////////////////////////////////////////////////////////////
67+
// The lines below were modified, copied and pasted out of the audio recorder object
68+
// (which never really needed a UI).
69+
// *** THE ACT OF MOVING THEM FROM A COMPONENT TO APP.TSX MADE THE INTERFACE WORK ***
70+
// *** Probably has to do with parent/child component state something idk ***
71+
////////////////////////////////////////////////////////////////////////////////
6572

73+
// Audio-related states and refs
74+
// State for whether we have microphone permissions - is set to true on first trip to playmode
75+
const [permission, setPermission] = useState(false);
76+
// Assorted audio-related objects in need of reference
77+
// Tend to be re-created upon starting a recording
78+
const mediaRecorder = useRef<MediaRecorder>(
79+
new MediaRecorder(new MediaStream()),
80+
);
81+
const [stream, setStream] = useState<MediaStream>(new MediaStream());
82+
const [audioChunks, setAudioChunks] = useState<Blob[]>([]);
83+
84+
const audioContextRef = useRef<any>(null);
85+
const analyserRef = useRef<any>(null);
86+
const dataArrayRef = useRef<any>(null);
87+
const startTimeRef = useRef<any>(null);
88+
89+
// Audio-related functions
6690
/////////////////////////////////////////////////////////
67-
// The code below updates the timestamp but is not yet tied to the API
68-
// This could be moved to any sub-component (e.g., ScoreDisplay, AudioPlayer)
69-
// or made its own invisible component - OR,
70-
// we will re-synchronize whenever the audiorecorder posts, in which case this should
71-
// be handled there
72-
const UPDATE_INTERVAL = 500; // milliseconds between updates to timestamp and rate
91+
// This function sends a synchronization request and updates the state with the result
92+
const UPDATE_INTERVAL = 100;
7393

74-
useEffect(() => {
75-
const getAPIData = async () => {
76-
const {
77-
playback_rate: newPlayRate,
78-
estimated_position: estimated_position,
79-
} = await synchronize(state.sessionToken, [0], state.timestamp);
80-
console.log("New play rate:", newPlayRate);
81-
console.log("New timestamp:", estimated_position);
94+
const getAPIData = async () => {
95+
analyserRef.current?.getByteTimeDomainData(dataArrayRef.current);
96+
const {
97+
playback_rate: newPlayRate,
98+
estimated_position: estimated_position,
99+
} = await synchronize(state.sessionToken, Array.from(dataArrayRef.current), state.timestamp);
82100

83-
dispatch({
84-
type: "increment",
85-
time: estimated_position,
86-
rate: 1,
87-
});
101+
dispatch({
102+
type: "increment",
103+
time: estimated_position,
104+
rate: newPlayRate,
105+
});
106+
}
107+
108+
// This function established new recording instances when re-entering play mode
109+
const startRecording = async () => {
110+
// It's possible some of these can be removed; not sure which relate to the
111+
// making of the recorded object we don't need and which relate to the
112+
// buffer we send to the backend.
113+
startTimeRef.current = Date.now();
114+
//create new Media recorder instance using the stream
115+
const media = new MediaRecorder(stream, { mimeType: "audio/webm" });
116+
//set the MediaRecorder instance to the mediaRecorder ref
117+
mediaRecorder.current = media;
118+
//invokes the start method to start the recording process
119+
mediaRecorder.current.start();
120+
let localAudioChunks: Blob[] = [];
121+
mediaRecorder.current.ondataavailable = (event) => {
122+
if (typeof event.data === "undefined") return;
123+
if (event.data.size === 0) return;
124+
localAudioChunks.push(event.data);
88125
};
126+
setAudioChunks(localAudioChunks);
89127

90-
// Start polling
91-
setInterval(() => {
92-
if (state.playing) {
93-
getAPIData();
128+
audioContextRef.current = new window.AudioContext();
129+
const source = audioContextRef.current.createMediaStreamSource(stream);
130+
analyserRef.current = audioContextRef.current.createAnalyser();
131+
analyserRef.current.fftSize = 2048;
132+
source.connect(analyserRef.current);
133+
134+
const bufferLength = analyserRef.current.frequencyBinCount;
135+
dataArrayRef.current = new Uint8Array(bufferLength);
136+
137+
getAPIData(); // run the first call
138+
};
139+
140+
//stops the recording instance
141+
const stopRecording = () => {
142+
mediaRecorder.current.stop();
143+
audioContextRef.current?.close();
144+
};
145+
146+
// Function to get permission to use browser microphone
147+
const getMicrophonePermission = async () => {
148+
if ("MediaRecorder" in window) {
149+
try {
150+
const streamData = await navigator.mediaDevices.getUserMedia({
151+
audio: true,
152+
video: false,
153+
});
154+
setPermission(true);
155+
setStream(streamData);
156+
} catch (err) {
157+
alert((err as Error).message);
94158
}
95-
}, UPDATE_INTERVAL);
96-
}, [state.playing, state.timestamp, state.sessionToken]);
97-
// The "could be moved into any subcomponent" comment refers to the above
98-
///////////////////////////////////////////////////////////////////////////////
159+
} else {
160+
alert("The MediaRecorder API is not supported in your browser.");
161+
}
162+
};
163+
164+
/////////////////////////////////////////////
165+
// Audio-related effects
166+
// Get microphone permission on first time entering play state
167+
useEffect(() => {
168+
if (!permission) getMicrophonePermission();
169+
}, [state.inPlayMode]);
170+
171+
// Start and stop recording when player is or isn't playing
172+
useEffect(() => {
173+
if (state.playing) startRecording();
174+
else stopRecording();
175+
}, [state.playing]);
176+
177+
// Keep synchronizing while playing
178+
useEffect(() => {
179+
if (state.playing) setTimeout(getAPIData, UPDATE_INTERVAL);
180+
}, [state.timestamp])
99181

100182
////////////////////////////////////////////////////////////////////////////////
101183
// Render the component's UI
102184
////////////////////////////////////////////////////////////////////////////////
103185
return (
104186
<SafeAreaView style={styles.container}>
105187
{/* Provides safe area insets for mobile devices */}
106-
<AudioRecorder state={state} dispatch={dispatch} />
107-
<Text style={styles.title}>Companion, the digital accompanist</Text>
108-
109-
<View style={styles.button_wrapper}>
110-
<Score_Select state={state} dispatch={dispatch} />
111-
<TempoBox
188+
<View style={styles.menu_bar}>
189+
<Image source={{ uri: './assets/companion.png' }} style={styles.logo}/>
190+
<Return_Button
112191
state={state}
113192
dispatch={dispatch}
114-
wrapper_style={styles.tempo_box}
115-
text_input_style={styles.text_input}
116-
label_text_style={styles.label}
117-
/>
118-
<SynthesizeButton
119-
state={state}
120-
dispatch={dispatch}
121-
button_style={styles.synthesize_button}
193+
button_format={styles.button_format}
122194
text_style={styles.button_text}
123195
/>
124196
<Start_Stop_Button
125197
state={state}
126198
dispatch={dispatch}
127-
button_style={styles.play_button}
199+
button_format={styles.button_format}
128200
text_style={styles.button_text}
129201
/>
130-
<MeasureSetBox
131-
state={state}
132-
dispatch={dispatch}
133-
wrapper_style={styles.measure_box}
134-
text_input_style={styles.text_input}
135-
button_style={styles.reset_button}
136-
button_text_style={styles.button_text}
137-
label_text_style={styles.label}
138-
/>
202+
{
203+
state.inPlayMode ?
204+
<MeasureSetBox
205+
state={state}
206+
dispatch={dispatch}
207+
button_style={styles.button_format}
208+
button_text_style={styles.button_text}
209+
/>
210+
:
211+
<TempoBox
212+
state={state}
213+
dispatch={dispatch}
214+
label_text_style={styles.button_text}
215+
/>
216+
}
217+
</View>
218+
<View style={styles.main_area}>
219+
{ // List of scores, show when not in play mode
220+
state.inPlayMode || <Score_Select state={state} dispatch={dispatch} /> }
221+
<ScoreDisplay state={state} dispatch={dispatch} />
139222
</View>
140-
<ScoreDisplay state={state} dispatch={dispatch} />
141223
<StatusBar style="auto" />
142224
{/* Automatically adjusts the status bar style */}
143225
<AudioPlayer state={state} />
@@ -154,65 +236,37 @@ const styles = StyleSheet.create({
154236
justifyContent: "center", // Center children vertically
155237
padding: 16, // Add padding around the container
156238
},
157-
title: {
158-
fontSize: 20, // Set the font size for the title
159-
marginBottom: 20, // Add space below the title
160-
},
161-
label: {
162-
fontSize: 16,
163-
},
164-
synthesize_button: {
165-
flex: 0.2,
166-
borderColor: "black",
167-
borderRadius: 15,
168-
backgroundColor: "lightblue",
169-
justifyContent: "center",
170-
},
171-
play_button: {
172-
flex: 0.2,
173-
borderColor: "black",
174-
borderRadius: 15,
175-
backgroundColor: "lightblue",
176-
justifyContent: "center",
177-
},
178-
reset_button: {
179-
flex: 0.8,
239+
button_format: {
180240
borderColor: "black",
181241
borderRadius: 15,
182242
backgroundColor: "lightblue",
243+
justifyContent: "center"
183244
},
184245
button_text: {
185-
fontSize: 20,
246+
fontSize: 24,
186247
textAlign: "center",
187248
},
188-
button_wrapper: {
189-
flex: 1,
249+
menu_bar: {
250+
flex: 0,
190251
flexDirection: "row",
191252
justifyContent: "space-between",
192-
padding: 10,
193253
backgroundColor: "lightgray",
194254
width: "100%",
195-
minHeight: 72,
255+
minHeight: 100,
196256
},
197-
measure_box: {
198-
flexDirection: "row",
199-
padding: 10,
200-
justifyContent: "space-between",
201-
width: "40%",
202-
flex: 0.4,
203-
height: "80%",
204-
},
205-
tempo_box: {
257+
main_area: {
258+
flex: 1,
206259
flexDirection: "row",
207-
padding: 10,
208260
justifyContent: "space-between",
209-
width: "20%",
210-
flex: 0.2,
261+
backgroundColor: "white",
262+
width: "100%",
211263
height: "80%",
212264
},
213-
text_input: {
265+
logo: {
214266
backgroundColor: "white",
215-
flex: 0.3,
267+
flex: 0.25,
268+
width: "25%",
216269
height: "100%",
270+
resizeMode: 'contain'
217271
},
218272
});

frontend/companion-app/Dispatch.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,22 @@ const reducer_function = (state: any, action: any) => {
3131
timestamp: action.time as number,
3232
},
3333
};
34+
35+
case "swap_mode":
36+
return {
37+
...state,
38+
...{
39+
inPlayMode: !state.inPlayMode
40+
}
41+
}
3442

3543
// When resetting, move the cursor, then adjust the timestamp accordingly and reset the playback rate
3644
case "reset":
3745
console.log("It should be resetting now.");
3846
var reset_time =
3947
(60 * state.time_signature.Numerator * (state.resetMeasure - 1)) /
4048
state.synth_tempo;
41-
state.sound?.setPositionAsync(reset_time * 1000);
49+
state.accompanimentSound.setPositionAsync(reset_time * 1000);
4250
return {
4351
...state,
4452
...{ playing: false, playRate: 1.0, timestamp: reset_time },
20.1 KB
Loading

frontend/companion-app/components/AudioPlayer.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ export function AudioPlayer({
2626

2727
useEffect(() => {
2828
const updateWhetherPlaying = async () => {
29-
console.log("Playing:", state.playing);
30-
console.log("Sound:", state.accompanimentSound);
3129
if (state.accompanimentSound) {
3230
if (state.playing) {
3331
await state.accompanimentSound.playAsync();

0 commit comments

Comments
 (0)