├── 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(