Skip to content

Commit

Permalink
feat: implemented metronome (#134)
Browse files Browse the repository at this point in the history
* fix: refactored Examples mapping

* feat: implemented custom slider, props does not work yet

* feat: added changing oscillator type button

* feat: implemented custom slider

* refactor: fixed custom slider and added button to switch oscillator type

* chore: remove unused packages

* chore: cleanup pod lockfile

* fix: applied changes requested in PR to Oscillator and App.tsx

* fix: fixed slider and applied changes requested in PR

* feat: implemented metronome

* fix: removed react-native-community/slider from deps

* fix: fixed constant name typo

* fix: applied changes requested in PR

* refactor: changed theme color and refecatored metronome play pause system

---------

Co-authored-by: Maciej Makowski <[email protected]>
Co-authored-by: Michal Sek <[email protected]>
  • Loading branch information
3 people authored Sep 25, 2024
1 parent 6d62cd2 commit 10a0cef
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 10 deletions.
2 changes: 1 addition & 1 deletion apps/common-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const App: FC = () => {
<Stack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: colors.darkblue,
backgroundColor: colors.main,
},
headerTintColor: colors.white,
}}
Expand Down
2 changes: 1 addition & 1 deletion apps/common-app/src/components/Slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const styles = StyleSheet.create({
sliderTrack: {
width: SLIDER_WIDTH,
height: HANDLE_SIZE + 10,
backgroundColor: colors.darkblue,
backgroundColor: colors.main,
borderRadius: 25,
justifyContent: 'center',
padding: layout.spacing,
Expand Down
156 changes: 154 additions & 2 deletions apps/common-app/src/examples/Metronome/Metronome.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,161 @@
import { FC } from 'react';
import React, { useState, useEffect, useRef, FC } from 'react';
import { Text, Button } from 'react-native';
import { AudioContext } from 'react-native-audio-api';

import Slider from '../../components/Slider';
import Container from '../../components/Container';
import { colors } from '../../styles';

const SCHEDULE_INTERVAL_MS = 25;
const SCHEDULE_AHEAD_TIME = 0.1;

const DOWN_BEAT_FREQUENCY = 1000;
const REGULAR_BEAT_FREQUENCY = 500;

const INITIAL_GAIN = 1;
const FADE_OUT_START_GAIN = 0.5;
const FADE_OUT_END_GAIN = 0.001;

const FADE_OUT_START_TIME = 0.01;
const FADE_OUT_END_TIME = 0.03;

const Metronome: FC = () => {
return <Container centered={true} />;
const [bpm, setBpm] = useState(120);
const [beatsPerBar, setBeatsPerBar] = useState(4);
const [isPlaying, setIsPlaying] = useState(false);

const audioContextRef = useRef<null | AudioContext>(null);
const intervalRef = useRef<null | NodeJS.Timeout>(null);
const nextNoteTimeRef = useRef(0.0);
const currentBeatRef = useRef(0);

const handlePlay = () => {
if (!audioContextRef.current) {
audioContextRef.current = new AudioContext();
}
setIsPlaying(true);
currentBeatRef.current = 0;
nextNoteTimeRef.current =
audioContextRef.current.currentTime + SCHEDULE_INTERVAL_MS / 1000;
intervalRef.current = setInterval(scheduler, SCHEDULE_INTERVAL_MS);
};

const handlePause = () => {
setIsPlaying(false);

if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};

const handlePlayPause = () => {
if (isPlaying) {
handlePause();

return;
}

handlePlay();
};

const handleBpmChange = (newBpm: number) => {
setBpm(newBpm);
handlePause();
};

const handleBeatsPerBarChange = (newBeatsPerBar: number) => {
setBeatsPerBar(newBeatsPerBar);
handlePause();
};

const scheduler = () => {
if (!audioContextRef.current) {
return;
}

while (
nextNoteTimeRef.current <
audioContextRef.current.currentTime + SCHEDULE_AHEAD_TIME
) {
playNote(currentBeatRef.current, nextNoteTimeRef.current);
nextNote();
}
};

const nextNote = () => {
const secondsPerBeat = 60.0 / bpm;
nextNoteTimeRef.current += secondsPerBeat;

currentBeatRef.current += 1;
if (currentBeatRef.current === beatsPerBar) {
currentBeatRef.current = 0;
}
};

const playNote = (beatNumber: number, time: number) => {
if (!audioContextRef.current) {
return;
}

const oscillator = audioContextRef.current.createOscillator();
const gain = audioContextRef.current.createGain();

oscillator.frequency.value =
beatNumber % beatsPerBar === 0
? DOWN_BEAT_FREQUENCY
: REGULAR_BEAT_FREQUENCY;

gain.gain.setValueAtTime(INITIAL_GAIN, time);
gain.gain.linearRampToValueAtTime(
FADE_OUT_START_GAIN,
time + FADE_OUT_START_TIME
);
gain.gain.linearRampToValueAtTime(
FADE_OUT_END_GAIN,
time + FADE_OUT_END_TIME
);

oscillator.connect(gain);
gain.connect(audioContextRef.current.destination);

oscillator.start(time);
oscillator.stop(time + FADE_OUT_END_TIME);
};

useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);

return (
<Container centered>
<Button
color={colors.main}
onPress={handlePlayPause}
title={isPlaying ? 'Pause' : 'Play'}
/>
<Text>BPM: {bpm}</Text>
<Slider
value={bpm}
onValueChange={handleBpmChange}
minimumValue={30}
maximumValue={240}
step={1}
/>
<Text>Beats per bar: {beatsPerBar}</Text>
<Slider
value={beatsPerBar}
onValueChange={handleBeatsPerBarChange}
minimumValue={1}
maximumValue={8}
step={1}
/>
</Container>
);
};

export default Metronome;
8 changes: 4 additions & 4 deletions apps/common-app/src/examples/Oscillator/Oscillator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,9 @@ const Oscillator: FC = () => {
}, []);

return (
<Container centered={true}>
<Container centered>
<Button
color={colors.darkblue}
color={colors.main}
title={isPlaying ? 'Pause' : 'Play'}
onPress={handlePlayPause}
/>
Expand Down Expand Up @@ -187,11 +187,11 @@ const styles = StyleSheet.create({
padding: layout.spacing,
marginHorizontal: 5,
borderWidth: 1,
borderColor: colors.darkblue,
borderColor: colors.main,
borderRadius: layout.radius,
},
activeOscillatorButton: {
backgroundColor: colors.darkblue,
backgroundColor: colors.main,
},
oscillatorButtonText: {
color: colors.black,
Expand Down
3 changes: 1 addition & 2 deletions apps/common-app/src/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ export const layout = {

export const colors = {
white: '#ffffff',
darkblue: '#001a72',
blue: '#007AFF',
main: '#428ce7',
border: 'rgba(0,0,0,0.1)',
black: '#000000',
};

0 comments on commit 10a0cef

Please sign in to comment.