React Native App for learning Piano Major Scales

Today I would like to share, Piano Major Scales App, done by me ‘just for fun’ and as a ‘proof of concept’. If you want you can make it more real and interesting and take things to the next level.

Logic of this app is that there are 12 piano major scales. With every scale there are 8 Notes associated and every note has specific fingering. You can check this website to understand more about piano major scales.

The main challenge was to do the UI of the App and make it look like piano keys even without using any image. Rest of the code is quite simple. Lets check the code of the app.

There are two screen used, one for listing Major scales and another one for rendering relative notes and fingering. Below is the code for Major scale screen :

import React, { Component } from 'react';
import {
  AppRegistry,
  StyleSheet,
  Text,
  View,
  TouchableOpacity,
  StatusBar,
  Dimensions,
  ListView
} from 'react-native';

const { width, height } = Dimensions.get('window')

const PianoMajorScales = [ "C", "D", "E", "F", "G", "A", "B", "Db","Eb","Gb","Ab", "Bb" ] //12

const scales = { "C": {"C" : 1, "D" : 2, "E" : 3, "F" : 4, "G" : 5, "A" : 6, "B" : 7},
                "D": {"D" : 1, "E" : 2, "F#" : 3, "G" : 4, "A" : 5, "B" : 6, "C#": 7},
                "E": {"E" : 1, "F#" : 2, "G#" : 3, "A" : 4, "B" : 5, "C#" : 6, "D#": 7 },
                "F": {"F" : 1, "G" : 2, "A" : 3, "Bd" : 4, "C" : 5, "D" : 6, "E": 7 },
                "G": {"G" : 1, "A" : 2, "B" : 3, "C" : 4, "D" : 5, "E" : 6, "F#": 7 },
                "A": {"A" : 1, "B" : 2, "C#" : 3, "D" : 4, "E" : 5, "F#" : 6, "G#": 7 },
                "B": {"B" : 1, "C#" : 2, "D#" : 3, "E" : 4, "F#" : 5, "G#" : 6, "A#": 7 },
                "Db": {"Db" : 1, "Eb" : 2, "F" : 3, "Gb" : 4, "Ab" : 5, "Bd" : 6, "C": 7 },
                "Eb": {"Eb" : 1, "F" : 2, "G" : 3, "Ab" : 4, "Bb" : 5, "C" : 6, "D": 7 },
                "Gb": {"Gb" : 1, "Ab" : 2, "Bd" : 3, "Cb" : 4, "Db" : 5, "Eb" : 6, "F": 7 },
                "Ab": {"Ab" : 1, "Bb" : 2, "C" : 3, "Db" : 4, "Eb" : 5, "F" : 6, "G": 7 },
                "Bd": {"Bd" : 1, "C" : 2, "D" : 3, "Eb" : 4, "F" : 5, "G" : 6, "A": 7 },
}

export default class Home extends Component {
  constructor(props) {
     super(props);
     const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
     this.state = {
       dataSource: ds.cloneWithRows(PianoMajorScales)
     }
  }
  onpress(){

  }
  _onPressButton(props){
    this.props.navigation.navigate('Notes', {scale: props, notes: scales[props]})
  }
  renderRow(props){
    return(
  <TouchableOpacity  style={styles.containerRow} onPress={() => this._onPressButton(props) }>
    <View style={{ backgroundColor: "#DCDCDC", width: width, paddingTop: 10, paddingBottom: 10}} >
      <Text style={styles.text}>
        {`${props}`} Major
      </Text>
    </View>
  </TouchableOpacity>
  )
  }
  render() {

    return (
    <View style={{flex:1, backgroundColor: '#F5FCFF' }}>
      <StatusBar barStyle="light-content"/>
      <View style={styles.toolbar}>
        <TouchableOpacity onPress={() => this.onpress() }><Text style={styles.toolbarButton}></Text></TouchableOpacity>
                    <Text style={styles.toolbarTitle}>Piano Major Scales</Text>
                    <Text style={styles.toolbarButton}></Text>
      </View>

      <ListView
        enableEmptySections={true}
        dataSource={this.state.dataSource}
        renderRow={(rowData) => this.renderRow(rowData)}
      />
    </View>
    );
    else
      return(
      <View>

      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  containerRow: {
    flex: 1,
    padding: 12,
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  text: {
    fontSize: width*3/100,
    marginLeft: 12,
    alignSelf: "center",
    fontWeight:'bold',
  },
  toolbar:{
      backgroundColor:'#000000',
      paddingTop:40,
      paddingBottom:20,
      flexDirection:'row'
  },
  toolbarButton:{
      width: 50,
      color:'#fff',
      textAlign:'center'
  },
  toolbarTitle:{
      fontSize: width*3/100,
      color:'#fff',
      textAlign:'center',
      fontWeight:'bold',
      flex:1
  }
});

In the above code there is an array of Piano Major Scales and a json data of piano major scales with respective notes. Now, when the user taps any particular scale tab, he gets redirected to the notes screen with corresponding notes of the scale passed as navigation parameter.

Below is the code of Notes screen with piano UI :

import React, { Component } from 'react';
import {
  AppRegistry,
  StyleSheet,
  Text,
  View,
  StatusBar,
  TouchableOpacity,
  ScrollView,
  Dimensions
} from 'react-native';
var Sound = require('react-native-sound');
Sound.setCategory('Playback');
var whoosh = new Sound('piano.mp3', Sound.MAIN_BUNDLE, (error) => {
  if (error) {
    console.log('failed to load the sound', error);
    return;
  }
});
const { width, height } = Dimensions.get('window')
import _ from 'lodash'
const scales = {

"C": {"C" : [1,"C"],"Db" : [0,"Db"], "D" : [2,"D"], "Eb": [0,"D#"], "E" : [3,"E"], "F" : [4,"F"], "Gb": [0,"F#"], "G" : [5,"G"], "Ab" : [0,"G#"],  "A" : [6,"A"], "Bb": [0,"Bd"], "B" : [7,"B"]},

"D": {"C" : [0,"C"],"Db" : [7,"Db"], "D" : [1,"D"], "Eb": [0,"D#"], "E" : [2,"E"], "F" : [0,"F"], "Gb": [3,"F#"], "G" : [4,"G"], "Ab" : [0,"G#"],  "A" : [5,"A"], "Bb": [0,"Bd"], "B" : [6,"B"]},

"E": {"C" : [0,"C"],"Db" : [6,"C#"], "D" : [0,"D"], "Eb": [7,"D#"], "E" : [1,"E"], "F" : [0,"F"], "Gb": [2,"F#"], "G" : [0,"G"], "Ab" : [3,"G#"],  "A" : [4,"A"], "Bb": [0,"Bd"], "B" : [5,"B"]},

"F": {"C" : [5,"C"],"Db" : [0,"C#"], "D" : [6,"D"], "Eb": [0,"D#"], "E" : [7,"E"], "F" : [1,"F"], "Gb": [0,"F#"], "G" : [2,"G"], "Ab" : [0,"G#"],  "A" : [3,"A"], "Bb": [4,"Bd"], "B" : [0,"B"]},

"G": {"C" : [4,"C"],"Db" : [0,"C#"], "D" : [5,"D"], "Eb": [0,"D#"], "E" : [6,"E"], "F" : [0,"F"], "Gb": [7,"F#"], "G" : [1,"G"], "Ab" : [0,"G#"],  "A" : [2,"A"], "Bb": [0,"Bd"], "B" : [3,"B"]},

"A": {"C" : [0,"C"],"Db" : [3,"C#"], "D" : [4,"D"], "Eb": [0,"D#"], "E" : [5,"E"], "F" : [0,"F"], "Gb": [6,"F#"], "G" : [0,"G"], "Ab" : [7,"G#"],  "A" : [1,"A"], "Bb": [0,"Bd"], "B" : [2,"B"]},

"B": {"C" : [0,"C"],"Db" : [2,"C#"], "D" : [0,"D"], "Eb": [3,"D#"], "E" : [4,"E"], "F" : [0,"F"], "Gb": [5,"F#"], "G" : [0,"G"], "Ab" : [6,"G#"],  "A" : [1,"A"], "Bb": [7,"A#"], "B" : [0,"B"]},

"Db": {"C" : [7,"C"],"Db" : [1,"Db"], "D" : [0,"D"], "Eb": [2,"Eb"], "E" : [0,"E"], "F" : [3,"F"], "Gb": [4,"Gb"], "G" : [0,"G"], "Ab" : [5,"Ab"],  "A" : [0,"A"], "Bb": [6,"Bd"], "B" : [0,"B"]},

"Eb": {"C" : [6,"C"],"Db" : [0,"Db"], "D" : [7,"D"], "Eb": [1,"Eb"], "E" : [0,"E"], "F" : [2,"F"], "Gb": [0,"Gb"], "G" : [3,"G"], "Ab" : [4,"Ab"],  "A" : [0,"A"], "Bb": [5,"Bd"], "B" : [0,"B"]},

"Gb": {"C" : [0,"C"],"Db" : [5,"Db"], "D" : [0,"D"], "Eb": [6,"Eb"], "E" : [0,"E"], "F" : [7,"F"], "Gb": [1,"Gb"], "G" : [0,"G"], "Ab" : [2,"Ab"],  "A" : [0,"A"], "Bb": [3,"Bd"], "B" : [4,"Cb"]},

"Ab": {"C" : [3,"C"],"Db" : [4,"Db"], "D" : [0,"D"], "Eb": [5,"Eb"], "E" : [0,"E"], "F" : [6,"F"], "Gb": [0,"Gb"], "G" : [7,"G"], "Ab" : [1,"Ab"],  "A" : [0,"A"], "Bb": [2,"Bd"], "B" : [0,"Cb"]},

"Bb": {"C" : [2,"C"],"Db" : [0,"Db"], "D" : [3,"D"], "Eb": [4,"Eb"], "E" : [0,"E"], "F" : [5,"F"], "Gb": [0,"Gb"], "G" : [6,"G"], "Ab" : [0,"Ab"],  "A" : [7,"A"], "Bb": [1,"Bd"], "B" : [0,"Cb"]},

}
export default class Notes extends Component {
  constructor(props) {
     super(props);

     this.state = {
        scale: this.props.navigation.state.params.scale,
        finger: ''
     }
  }

  onpressBack(){
    const {goBack} = this.props.navigation
    goBack()
  }

  _onPressKey(key){
    const finger = scales[this.state.scale][key][0]

    if(finger){

    /*  whoosh.play((success) => {
        if (success) {
          const finger = scales[_this.state.scale][rowData]
          _this.setState({ finger })
          console.log('successfully finished playing');
        } else {
          console.log('playback failed due to audio decoding errors');
          // reset the player to its uninitialized state (android only)
          // this is the only option to recover after an error occured and use the player again
          whoosh.reset();
        }
      });*/

      this.setState({ finger })

    }

  }

  render() {
    return (
      <View style={{flex:1, backgroundColor: '#F5FCFF' }}>
      <StatusBar barStyle="light-content"/>
      <View style={styles.toolbar}>
        <TouchableOpacity onPress={() => this.onpressBack() }><Text style={styles.toolbarButton}>Back</Text></TouchableOpacity>
                    <Text style={styles.toolbarTitle}>Notes for { this.state.scale }</Text>
                    <Text style={styles.toolbarButton}></Text>
      </View>

{/* piano start*/}
<View style={{ flex:.20 }}></View>
<View style={{ flex:1, flexDirection: 'row' }}>
  <View style={{ flex:.80 }}>

      <ScrollView>

      <TouchableOpacity style={styles.container} onPress={() => this._onPressKey("C") }>
        <View>
        {
          scales[this.state.scale]["C"][0] ? (<Text style={{ fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["C"][1]}</Text>) :
          (<Text style={{ color: "white", fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["C"][1]}</Text>)
        }


        </View>
      </TouchableOpacity>

      <View style={styles.container2}>
          <View style={styles.container3}>
          <View
            style={{
              flex:1,
              borderBottomWidth: 1,
              borderBottomColor: 'black',
            }}
          />
        </View>
        <TouchableOpacity style={[styles.container,{backgroundColor: "black"}]} onPress={() => this._onPressKey("Db") }>
        <View>

        </View>
        </TouchableOpacity>
        {
          scales[this.state.scale]["Db"][0] ? <Text style={{ fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["Db"][1]}</Text> :
          <Text style={{ color: "white", fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["Db"][1]}</Text>
        }

    </View>


    <TouchableOpacity style={styles.container} onPress={() => this._onPressKey("D") }>
      <View>
      {
        scales[this.state.scale]["D"][0] ? <Text style={{ fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["D"][1]}</Text> :
        <Text style={{ color: "white", fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["D"][1]}</Text>
      }
      </View>
    </TouchableOpacity>


    <View style={styles.container2}>
        <View style={styles.container3}>
        <View
          style={{
            flex:1,
            borderBottomWidth: 1,
            borderBottomColor: 'black',
          }}
        />
      </View>
      <TouchableOpacity style={[styles.container,{backgroundColor: "black"}]} onPress={() => this._onPressKey("Eb") }>
      <View>

      </View>
      </TouchableOpacity>
      {
        scales[this.state.scale]["Eb"][0] ? <Text style={{ fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["Eb"][1]}</Text> :
        <Text style={{ color: "white", fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["Eb"][1]}</Text>
      }

  </View>

  <TouchableOpacity style={styles.container} onPress={() => this._onPressKey("E") }>
    <View>

     {
       scales[this.state.scale]["E"][0] ? <Text style={{ fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["E"][1]}</Text> :
       <Text style={{ color: "white", fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["E"][1]}</Text>
     }
    </View>
  </TouchableOpacity>


  <View
    style={{
      flex:1,
      borderBottomWidth: 1,
      borderBottomColor: 'black',
      marginTop: 20,
      marginBottom: 20
    }}
  />



  <TouchableOpacity style={styles.container} onPress={() => this._onPressKey("F") }>
    <View>

     {
       scales[this.state.scale]["F"][0] ? <Text style={{ fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["F"][1]}</Text> :
       <Text style={{ color: "white", fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["F"][1]}</Text>
     }
    </View>
  </TouchableOpacity>

  <View style={styles.container2}>
      <View style={styles.container3}>
      <View
        style={{
          flex:1,
          borderBottomWidth: 1,
          borderBottomColor: 'black',
        }}
      />
    </View>
    <TouchableOpacity style={[styles.container,{backgroundColor: "black"}]} onPress={() => this._onPressKey("Gb") }>
    <View>

    </View>
    </TouchableOpacity>
    {
      scales[this.state.scale]["Gb"][0] ? <Text style={{ fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["Gb"][1]}</Text> :
      <Text style={{ color: "white", fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["Gb"][1]}</Text>
    }

</View>

<TouchableOpacity style={styles.container} onPress={() => this._onPressKey("G") }>
  <View>

   {
     scales[this.state.scale]["G"][0] ? <Text style={{ fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["G"][1]}</Text> :
     <Text style={{ color: "white", fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["G"][1]}</Text>
   }
  </View>
</TouchableOpacity>

<View style={styles.container2}>
    <View style={styles.container3}>
    <View
      style={{
        flex:1,
        borderBottomWidth: 1,
        borderBottomColor: 'black',
      }}
    />
  </View>
  <TouchableOpacity style={[styles.container,{backgroundColor: "black"}]} onPress={() => this._onPressKey("Ab") }>
  <View>

  </View>
  </TouchableOpacity>
  {
    scales[this.state.scale]["Ab"][0] ? <Text style={{ fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["Ab"][1]}</Text> :
    <Text style={{ color: "white", fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["Ab"][1]}</Text>
  }

</View>

<TouchableOpacity style={styles.container} onPress={() => this._onPressKey("A") }>
  <View>

   {
     scales[this.state.scale]["A"][0] ? <Text style={{ fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["A"][1]}</Text> :
     <Text style={{ color: "white", fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["A"][1]}</Text>
   }
  </View>
</TouchableOpacity>

<View style={styles.container2}>
    <View style={styles.container3}>
    <View
      style={{
        flex:1,
        borderBottomWidth: 1,
        borderBottomColor: 'black',
      }}
    />
  </View>
  <TouchableOpacity style={[styles.container,{backgroundColor: "black"}]} onPress={() => this._onPressKey("Bb") }>
  <View>

  </View>
  </TouchableOpacity>
  {
    scales[this.state.scale]["Bb"][0] ? <Text style={{ fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["Bb"][1]}</Text> :
    <Text style={{ color: "white", fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["Bb"][1]}</Text>
  }

</View>

<TouchableOpacity style={styles.container} onPress={() => this._onPressKey("B") }>
  <View>

   {
     scales[this.state.scale]["B"][0] ? <Text style={{ fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["B"][1]}</Text> :
     <Text style={{ color: "white", fontWeight: "bold" ,transform: [{ rotate: '90deg'}]}} >{scales[this.state.scale]["B"][1]}</Text>
   }
  </View>
</TouchableOpacity>
      </ScrollView>
 </View>
 <View style={{ flex:.20,alignItems: 'center',justifyContent: 'center', }}>
<Text style={styles.finger} >{ this.state.finger }</Text>
</View>

</View>
{/* piano end*/}


      </View>
    );
  }
}

const styles = StyleSheet.create({

  container: {
    flex: 1,
    padding: 20,
    flexDirection: 'row',
    alignItems: 'center',
  },
  container2: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center',
  },
  container3: {
    flex: 1,
    paddingTop: 20,
    paddingBottom: 20,
    flexDirection: 'row',
    alignItems: 'center',
  },
  separator: {
      flex: 1,
      height: StyleSheet.hairlineWidth,
      backgroundColor: '#8E8E8E',
  },
  toolbar:{
      backgroundColor:'#000000',
      paddingTop:40,
      paddingBottom:20,
      flexDirection:'row'
  },
  toolbarButton:{
      width: 70,
      color:'#fff',
      textAlign:'center',
      fontSize: width*3/100,
  },
  toolbarTitle:{
      fontSize: width*3/100,
      color:'#fff',
      textAlign:'center',
      fontWeight:'bold',
      flex:1
  },
  list: {
        justifyContent: 'center',
        flexDirection: 'row',
        flexWrap: 'wrap'
    },
    itemContainer : {
      margin: 10,
      width: 100,
      height: 100,
      justifyContent: 'center',
      alignItems: 'center',
      backgroundColor: '#CCC',
    },
  item: {
      backgroundColor: '#CCC',
      margin: 10,
      width: 100,
      height: 100
  },
  note:{
    fontSize: width*10/100
  },
  finger:{
    fontSize: width*10/100,
    transform: [{ rotate: '90deg'}]
  }
});

In the above code I have created a map of major scale, notes and fingering using json data. When user taps a note, then respective fingering no shown. I dint have all the sounds for piano keys so i dint do the sound part, but, I have added a sample sound code just to show how sound can be implemented, if you have piano sound files. Its worth taking a look at how piano buttons are done with some illusion 🙂

Things to do

  1. Implement sound effect for all keys and use different sound effect on long press event.
  2. make the UI with 3d buttons.

If you are able to make this better please share your app. I would love to know your experience in comments.

A comprehensive guide on how to master the guitar C Sharp chord https://beginnerguitarhq.com/c-sharp-guitar-chord/

Video of this article :