├── assets
├── images
│ ├── icon.png
│ ├── favicon.png
│ ├── splash.png
│ └── adaptive-icon.png
└── fonts
│ └── SpaceMono-Regular.ttf
├── tsconfig.json
├── babel.config.js
├── .gitignore
├── components
├── StyledText.tsx
├── __tests__
│ └── StyledText-test.js
├── Themed.tsx
├── FrequencySlider.tsx
├── EditScreenInfo.tsx
├── EQTest.tsx
└── AudioPlayer.tsx
├── constants
├── Layout.ts
└── Colors.ts
├── README.md
├── .expo-shared
└── assets.json
├── hooks
├── useColorScheme.ts
└── useCachedResources.ts
├── App.tsx
├── screens
├── TabTwoScreen.tsx
├── NotFoundScreen.tsx
├── ModalScreen.tsx
└── TabOneScreen.tsx
├── app.json
├── navigation
├── LinkingConfiguration.ts
└── index.tsx
├── api
└── api.ts
├── types.tsx
└── package.json
/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/i/ear-training-app/master/assets/images/icon.png
--------------------------------------------------------------------------------
/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/i/ear-training-app/master/assets/images/favicon.png
--------------------------------------------------------------------------------
/assets/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/i/ear-training-app/master/assets/images/splash.png
--------------------------------------------------------------------------------
/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/i/ear-training-app/master/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/assets/fonts/SpaceMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/i/ear-training-app/master/assets/fonts/SpaceMono-Regular.ttf
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo']
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | dist/
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 |
13 | # macOS
14 | .DS_Store
15 |
--------------------------------------------------------------------------------
/components/StyledText.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { Text, TextProps } from './Themed';
4 |
5 | export function MonoText(props: TextProps) {
6 | return ;
7 | }
8 |
--------------------------------------------------------------------------------
/constants/Layout.ts:
--------------------------------------------------------------------------------
1 | import { Dimensions } from 'react-native';
2 |
3 | const width = Dimensions.get('window').width;
4 | const height = Dimensions.get('window').height;
5 |
6 | export default {
7 | window: {
8 | width,
9 | height,
10 | },
11 | isSmallDevice: width < 375,
12 | };
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Ear training app
2 | ================
3 |
4 | The client for the ear training app.
5 |
6 | ## Setup
7 |
8 | This app is built using expo. To get it up and running download you need
9 | expo-cli. `npm i -g expo-cli`.
10 |
11 | Run `expo start` to start the app.
12 |
13 |
14 | ## Structure
15 |
16 | no idea yet
17 |
--------------------------------------------------------------------------------
/.expo-shared/assets.json:
--------------------------------------------------------------------------------
1 | {
2 | "e997a5256149a4b76e6bfd6cbf519c5e5a0f1d278a3d8fa1253022b03c90473b": true,
3 | "af683c96e0ffd2cf81287651c9433fa44debc1220ca7cb431fe482747f34a505": true,
4 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
5 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
6 | }
7 |
--------------------------------------------------------------------------------
/components/__tests__/StyledText-test.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import renderer from 'react-test-renderer';
3 |
4 | import { MonoText } from '../StyledText';
5 |
6 | it(`renders correctly`, () => {
7 | const tree = renderer.create(Snapshot test!).toJSON();
8 |
9 | expect(tree).toMatchSnapshot();
10 | });
11 |
--------------------------------------------------------------------------------
/hooks/useColorScheme.ts:
--------------------------------------------------------------------------------
1 | import { ColorSchemeName, useColorScheme as _useColorScheme } from 'react-native';
2 |
3 | // The useColorScheme value is always either light or dark, but the built-in
4 | // type suggests that it can be null. This will not happen in practice, so this
5 | // makes it a bit easier to work with.
6 | export default function useColorScheme(): NonNullable {
7 | return _useColorScheme() as NonNullable;
8 | }
9 |
--------------------------------------------------------------------------------
/constants/Colors.ts:
--------------------------------------------------------------------------------
1 | const tintColorLight = '#2f95dc';
2 | const tintColorDark = '#fff';
3 |
4 | export default {
5 | light: {
6 | text: '#000',
7 | background: '#fff',
8 | tint: tintColorLight,
9 | tabIconDefault: '#ccc',
10 | tabIconSelected: tintColorLight,
11 | },
12 | dark: {
13 | text: '#fff',
14 | background: '#000',
15 | tint: tintColorDark,
16 | tabIconDefault: '#ccc',
17 | tabIconSelected: tintColorDark,
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/App.tsx:
--------------------------------------------------------------------------------
1 | import { StatusBar } from 'expo-status-bar';
2 | import React from 'react';
3 | import { SafeAreaProvider } from 'react-native-safe-area-context';
4 |
5 | import useCachedResources from './hooks/useCachedResources';
6 | import useColorScheme from './hooks/useColorScheme';
7 | import Navigation from './navigation';
8 |
9 | export default function App() {
10 | const isLoadingComplete = useCachedResources();
11 | const colorScheme = useColorScheme();
12 |
13 | if (!isLoadingComplete) {
14 | return null;
15 | } else {
16 | return (
17 |
18 |
19 |
20 |
21 | );
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/screens/TabTwoScreen.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { StyleSheet, TextInput } from 'react-native';
3 |
4 | import EditScreenInfo from '../components/EditScreenInfo';
5 | import { Text, View } from '../components/Themed';
6 | import { MultiAudioPlayer } from '../components/AudioPlayer';
7 | import { EQTest } from '../components/EQTest';
8 |
9 | export default function TabTwoScreen() {
10 | return (
11 |
12 |
13 |
14 | );
15 | }
16 |
17 | const styles = StyleSheet.create({
18 | container: {
19 | flex: 1,
20 | alignItems: 'center',
21 | justifyContent: 'center',
22 | },
23 | title: {
24 | fontSize: 20,
25 | fontWeight: 'bold',
26 | },
27 | separator: {
28 | marginVertical: 30,
29 | height: 1,
30 | width: '80%',
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "app",
4 | "slug": "app",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/images/icon.png",
8 | "scheme": "myapp",
9 | "userInterfaceStyle": "automatic",
10 | "splash": {
11 | "image": "./assets/images/splash.png",
12 | "resizeMode": "contain",
13 | "backgroundColor": "#ffffff"
14 | },
15 | "updates": {
16 | "fallbackToCacheTimeout": 0
17 | },
18 | "assetBundlePatterns": [
19 | "**/*"
20 | ],
21 | "ios": {
22 | "supportsTablet": true
23 | },
24 | "android": {
25 | "adaptiveIcon": {
26 | "foregroundImage": "./assets/images/adaptive-icon.png",
27 | "backgroundColor": "#ffffff"
28 | }
29 | },
30 | "web": {
31 | "favicon": "./assets/images/favicon.png"
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/navigation/LinkingConfiguration.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Learn more about deep linking with React Navigation
3 | * https://reactnavigation.org/docs/deep-linking
4 | * https://reactnavigation.org/docs/configuring-links
5 | */
6 |
7 | import { LinkingOptions } from '@react-navigation/native';
8 | import * as Linking from 'expo-linking';
9 |
10 | import { RootStackParamList } from '../types';
11 |
12 | const linking: LinkingOptions = {
13 | prefixes: [Linking.makeUrl('/')],
14 | config: {
15 | screens: {
16 | Root: {
17 | screens: {
18 | TabOne: {
19 | screens: {
20 | TabOneScreen: 'one',
21 | },
22 | },
23 | TabTwo: {
24 | screens: {
25 | TabTwoScreen: 'two',
26 | },
27 | },
28 | },
29 | },
30 | Modal: 'modal',
31 | NotFound: '*',
32 | },
33 | },
34 | };
35 |
36 | export default linking;
37 |
--------------------------------------------------------------------------------
/api/api.ts:
--------------------------------------------------------------------------------
1 | export class Api {
2 | baseUri: string;
3 |
4 | constructor(baseUri: string) {
5 | this.baseUri = baseUri;
6 | }
7 |
8 |
9 | public async getTest() {
10 | try {
11 | const response = await fetch(this.baseUri + '/test');
12 | const json = await response.json();
13 | json.items.forEach((item) => {
14 | item.original_audio_url = item.original_audio_url.replace('localhost', '10.21.21.8') + '/download.mp3';
15 | item.processed_audio_url = item.processed_audio_url.replace('localhost', '10.21.21.8') + '/download.mp3';
16 | })
17 | return json;
18 | } catch (error) {
19 | // Handle the error.
20 | console.log(error);
21 | }
22 | return null;
23 | }
24 |
25 | public async getAudios() {
26 | try {
27 | const response = await fetch(this.baseUri + '/audio');
28 | return await response.json();
29 | } catch (error) {
30 | // Handle the error.
31 | console.log(error);
32 | }
33 | return null;
34 | }
35 |
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/screens/NotFoundScreen.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { StyleSheet, TouchableOpacity } from 'react-native';
3 | import { Text, View } from '../components/Themed';
4 |
5 | import { RootStackScreenProps } from '../types';
6 |
7 | export default function NotFoundScreen({ navigation }: RootStackScreenProps<'NotFound'>) {
8 | return (
9 |
10 | This screen doesn't exist.
11 | navigation.replace('Root')} style={styles.link}>
12 | Go to home screen!
13 |
14 |
15 | );
16 | }
17 |
18 | const styles = StyleSheet.create({
19 | container: {
20 | flex: 1,
21 | alignItems: 'center',
22 | justifyContent: 'center',
23 | padding: 20,
24 | },
25 | title: {
26 | fontSize: 20,
27 | fontWeight: 'bold',
28 | },
29 | link: {
30 | marginTop: 15,
31 | paddingVertical: 15,
32 | },
33 | linkText: {
34 | fontSize: 14,
35 | color: '#2e78b7',
36 | },
37 | });
38 |
--------------------------------------------------------------------------------
/hooks/useCachedResources.ts:
--------------------------------------------------------------------------------
1 | import { FontAwesome } from '@expo/vector-icons';
2 | import * as Font from 'expo-font';
3 | import * as SplashScreen from 'expo-splash-screen';
4 | import * as React from 'react';
5 |
6 | export default function useCachedResources() {
7 | const [isLoadingComplete, setLoadingComplete] = React.useState(false);
8 |
9 | // Load any resources or data that we need prior to rendering the app
10 | React.useEffect(() => {
11 | async function loadResourcesAndDataAsync() {
12 | try {
13 | SplashScreen.preventAutoHideAsync();
14 |
15 | // Load fonts
16 | await Font.loadAsync({
17 | ...FontAwesome.font,
18 | 'space-mono': require('../assets/fonts/SpaceMono-Regular.ttf'),
19 | });
20 | } catch (e) {
21 | // We might want to provide this error information to an error reporting service
22 | console.warn(e);
23 | } finally {
24 | setLoadingComplete(true);
25 | SplashScreen.hideAsync();
26 | }
27 | }
28 |
29 | loadResourcesAndDataAsync();
30 | }, []);
31 |
32 | return isLoadingComplete;
33 | }
34 |
--------------------------------------------------------------------------------
/screens/ModalScreen.tsx:
--------------------------------------------------------------------------------
1 | import { StatusBar } from 'expo-status-bar';
2 | import * as React from 'react';
3 | import { Platform, StyleSheet } from 'react-native';
4 |
5 | import EditScreenInfo from '../components/EditScreenInfo';
6 | import { Text, View } from '../components/Themed';
7 |
8 | export default function ModalScreen() {
9 | return (
10 |
11 | Modal
12 |
13 |
14 |
15 | {/* Use a light status bar on iOS to account for the black space above the modal */}
16 |
17 |
18 | );
19 | }
20 |
21 | const styles = StyleSheet.create({
22 | container: {
23 | flex: 1,
24 | alignItems: 'center',
25 | justifyContent: 'center',
26 | },
27 | title: {
28 | fontSize: 20,
29 | fontWeight: 'bold',
30 | },
31 | separator: {
32 | marginVertical: 30,
33 | height: 1,
34 | width: '80%',
35 | },
36 | });
37 |
--------------------------------------------------------------------------------
/types.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Learn more about using TypeScript with React Navigation:
3 | * https://reactnavigation.org/docs/typescript/
4 | */
5 |
6 | import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
7 | import { CompositeScreenProps, NavigatorScreenParams } from '@react-navigation/native';
8 | import { NativeStackScreenProps } from '@react-navigation/native-stack';
9 |
10 | declare global {
11 | namespace ReactNavigation {
12 | interface RootParamList extends RootStackParamList {}
13 | }
14 | }
15 |
16 | export type RootStackParamList = {
17 | Root: NavigatorScreenParams | undefined;
18 | Modal: undefined;
19 | NotFound: undefined;
20 | };
21 |
22 | export type RootStackScreenProps = NativeStackScreenProps<
23 | RootStackParamList,
24 | Screen
25 | >;
26 |
27 | export type RootTabParamList = {
28 | TabOne: undefined;
29 | TabTwo: undefined;
30 | };
31 |
32 | export type RootTabScreenProps = CompositeScreenProps<
33 | BottomTabScreenProps,
34 | NativeStackScreenProps
35 | >;
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app",
3 | "version": "1.0.0",
4 | "main": "node_modules/expo/AppEntry.js",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web",
10 | "eject": "expo eject",
11 | "test": "jest --watchAll"
12 | },
13 | "jest": {
14 | "preset": "jest-expo"
15 | },
16 | "dependencies": {
17 | "@expo/ngrok": "^2.4.3",
18 | "@expo/vector-icons": "^12.0.0",
19 | "@react-navigation/bottom-tabs": "^6.0.5",
20 | "@react-navigation/native": "^6.0.2",
21 | "@react-navigation/native-stack": "^6.1.0",
22 | "cors": "^2.8.5",
23 | "expo": "~43.0.2",
24 | "expo-asset": "~8.4.3",
25 | "expo-av": "~10.1.3",
26 | "expo-constants": "~12.1.3",
27 | "expo-font": "~10.0.3",
28 | "expo-linking": "~2.4.2",
29 | "expo-splash-screen": "~0.13.5",
30 | "expo-status-bar": "~1.1.0",
31 | "expo-web-browser": "~10.0.3",
32 | "ngrok": "^4.2.2",
33 | "react": "17.0.1",
34 | "react-dom": "17.0.1",
35 | "react-native": "0.64.3",
36 | "react-native-safe-area-context": "3.3.2",
37 | "react-native-screens": "~3.8.0",
38 | "react-native-web": "0.17.1",
39 | "@react-native-community/slider": "4.1.12"
40 | },
41 | "devDependencies": {
42 | "@babel/core": "^7.12.9",
43 | "@types/react": "~17.0.21",
44 | "@types/react-native": "~0.64.12",
45 | "jest-expo": "~43.0.0",
46 | "typescript": "~4.3.5"
47 | },
48 | "private": true
49 | }
50 |
--------------------------------------------------------------------------------
/components/Themed.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Learn more about Light and Dark modes:
3 | * https://docs.expo.io/guides/color-schemes/
4 | */
5 |
6 | import * as React from 'react';
7 | import { Text as DefaultText, View as DefaultView } from 'react-native';
8 |
9 | import Colors from '../constants/Colors';
10 | import useColorScheme from '../hooks/useColorScheme';
11 |
12 | export function useThemeColor(
13 | props: { light?: string; dark?: string },
14 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark
15 | ) {
16 | const theme = useColorScheme();
17 | const colorFromProps = props[theme];
18 |
19 | if (colorFromProps) {
20 | return colorFromProps;
21 | } else {
22 | return Colors[theme][colorName];
23 | }
24 | }
25 |
26 | type ThemeProps = {
27 | lightColor?: string;
28 | darkColor?: string;
29 | };
30 |
31 | export type TextProps = ThemeProps & DefaultText['props'];
32 | export type ViewProps = ThemeProps & DefaultView['props'];
33 |
34 | export function Text(props: TextProps) {
35 | const { style, lightColor, darkColor, ...otherProps } = props;
36 | const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
37 |
38 | return ;
39 | }
40 |
41 | export function View(props: ViewProps) {
42 | const { style, lightColor, darkColor, ...otherProps } = props;
43 | const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
44 |
45 | return ;
46 | }
47 |
--------------------------------------------------------------------------------
/components/FrequencySlider.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Text, View } from '../components/Themed';
3 | import { Button, StyleSheet, TextInput } from 'react-native';
4 | import Slider from '@react-native-community/slider';
5 |
6 | export function FrequencySlider(props) {
7 | const [freq, setFreq] = React.useState(500);
8 | const onSlidingComplete = props.onSlidingComplete;
9 |
10 |
11 | const onTouchMove = function(val: number) {
12 | setFreq(logslider(val));
13 | };
14 |
15 |
16 | const done = function(val: number) {
17 | const guess = logslider(val);
18 | onSlidingComplete(freq);
19 | }
20 |
21 | return (
22 |
23 | {freq}
24 |
34 |
35 | );
36 | }
37 |
38 | function logslider(position: number) {
39 | // position will be between 0 and 100
40 | var minp = 0.0;
41 | var maxp = 1.0;
42 |
43 | var minv = Math.log(20);
44 | var maxv = Math.log(20000);
45 |
46 | // calculate adjustment factor
47 | var scale = (maxv-minv) / (maxp-minp);
48 |
49 | return Math.ceil(Math.exp(minv + scale*(position-minp)));
50 | }
51 |
52 | const styles = StyleSheet.create({
53 | freqSlider: {
54 | flex: 1,
55 | },
56 | slider: {
57 | width: 200,
58 | height: 40,
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/components/EditScreenInfo.tsx:
--------------------------------------------------------------------------------
1 | import * as WebBrowser from 'expo-web-browser';
2 | import React from 'react';
3 | import { StyleSheet, TouchableOpacity } from 'react-native';
4 |
5 | import Colors from '../constants/Colors';
6 | import { MonoText } from './StyledText';
7 | import { Text, View } from './Themed';
8 |
9 | export default function EditScreenInfo({ path }: { path: string }) {
10 | return (
11 |
12 |
13 |
17 | Open up the code for this screen:
18 |
19 |
20 |
24 | {path}
25 |
26 |
27 |
31 | Change any of the text, save the file, and your app will automatically update.
32 |
33 |
34 |
35 |
36 |
37 |
38 | Tap here if your app doesn't automatically update after making changes
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | function handleHelpPress() {
47 | WebBrowser.openBrowserAsync(
48 | 'https://docs.expo.io/get-started/create-a-new-app/#opening-the-app-on-your-phonetablet'
49 | );
50 | }
51 |
52 | const styles = StyleSheet.create({
53 | getStartedContainer: {
54 | alignItems: 'center',
55 | marginHorizontal: 50,
56 | },
57 | homeScreenFilename: {
58 | marginVertical: 7,
59 | },
60 | codeHighlightContainer: {
61 | borderRadius: 3,
62 | paddingHorizontal: 4,
63 | },
64 | getStartedText: {
65 | fontSize: 17,
66 | lineHeight: 24,
67 | textAlign: 'center',
68 | },
69 | helpContainer: {
70 | marginTop: 15,
71 | marginHorizontal: 20,
72 | alignItems: 'center',
73 | },
74 | helpLink: {
75 | paddingVertical: 15,
76 | },
77 | helpLinkText: {
78 | textAlign: 'center',
79 | },
80 | });
81 |
--------------------------------------------------------------------------------
/screens/TabOneScreen.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useEffect, useState } from 'react';
3 | import { StyleSheet, Button } from 'react-native';
4 | import { Audio } from 'expo-av';
5 |
6 | import EditScreenInfo from '../components/EditScreenInfo';
7 | import { Text, View } from '../components/Themed';
8 | import { RootTabScreenProps } from '../types';
9 |
10 |
11 | function AudioPlayer(props) {
12 | var playingIndex = 0;
13 | const playing = false;
14 | const sounds: Audio.Sound[] = [];
15 |
16 | const uris = props.uris;
17 |
18 |
19 | console.log(uris);
20 | const initialStatus = {
21 | shouldPlay: false,
22 | rate: 1.0,
23 | shouldCorrectPitch: false,
24 | volume: 1.0,
25 | isMuted: false,
26 | isLooping: true,
27 | };
28 |
29 |
30 | const makePlay = function(idx: number) {
31 | return function() {
32 | sounds[playingIndex].stopAsync();
33 | sounds[idx].playAsync();
34 | playingIndex = idx;
35 | };
36 | }
37 |
38 | const parts = [];
39 |
40 | for (var i = 0; i < uris.length; i++) {
41 | const source = { uri: uris[i] };
42 |
43 | try {
44 | sounds[i] = new Audio.Sound();
45 | sounds[i].loadAsync(
46 | source,
47 | initialStatus,
48 | false
49 | );
50 |
51 | const title = "BUTTON " + i;
52 |
53 | parts.push(