Help us understand the problem. What is going on with this article?

React Nativeでピアノを作る

More than 1 year has passed since last update.

はじめに

React Nativeでピアノを作ったのでソースコードの説明をします。
Githubのリポジトリはこちらです。

プロジェクトの立ち上げ

react-nativeのコマンドでプロジェクトを作ります。

react-native init piano
cd piano

ピアノ音源の準備

mp3形式で音源を準備します。
Githubのリポジトリにアップロードしているので、こちらをお使いください。

audio/
├── A.mp3
├── As.mp3
├── B.mp3
├── C.mp3
├── Cs.mp3
├── D.mp3
├── Ds.mp3
├── E.mp3
├── F.mp3
├── Fs.mp3
├── G.mp3
└── Gs.mp3

react-native-soundのインストール

React Nativeで音を鳴らすためのライブラリreact-native-soundをインストールします。

yarn add react-native-sound
react-native link react-native-sound

iOSではxcode上でプロジェクトファイルの中に音源をドラッグ&ドロップします。
Screen Shot 2019-02-27 at 18.29.57.png

Androidでは音源を android/app/src/main/res/raw のフォルダに入れます。

mkdir android/app/src/main/res/raw
cp audio/* android/app/src/main/res/raw/

ソースコード

ソースコードは以下になります。
順番に説明していきます。

App.jsx
import React from 'react';
import {
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
} from 'react-native';

import Sound from 'react-native-sound';

export default class App extends React.Component {
  constructor( props ){
    super( props );

    this.state = {
      colorC : "white",
      colorCs: "black",
      colorD : "white",
      colorDs: "black",
      colorE : "white",
      colorF : "white",
      colorFs: "black",
      colorG : "white",
      colorGs: "black",
      colorA : "white",
      colorAs: "black",
      colorB : "white",
    }

    this.sound = {};
    const soundList = [ "C", "Cs", "D", "Ds", "E", "F", "Fs", "G", "Gs", "A", "As", "B" ]
    soundList.forEach(note => {
      this.sound[note] = new Sound( note + ".mp3", Sound.MAIN_BUNDLE, error => {
        if ( error ) {
          console.log("failed to load the sound.", error);
        }
      })
    });
  }
  stroke ( note ) {
    switch ( note ) {
      case "C":
        this.setState({ colorC: "rgba(1, 1, 1, 0.1)" })
        break;
      case "Cs":
        this.setState({ colorCs: "rgba(0, 0, 0, 0.5)" })
        break;
      case "D":
        this.setState({ colorD: "rgba(1, 1, 1, 0.1)" })
        break;
      case "Ds":
        this.setState({ colorDs: "rgba(0, 0, 0, 0.5)" })
        break;
      case "E":
        this.setState({ colorE: "rgba(1, 1, 1, 0.1)" })
        break;
      case "F":
        this.setState({ colorF: "rgba(1, 1, 1, 0.1)" })
        break;
      case "Fs":
        this.setState({ colorFs: "rgba(0, 0, 0, 0.5)" })
        break;
      case "G":
        this.setState({ colorG: "rgba(1, 1, 1, 0.1)" })
        break;
      case "Gs":
        this.setState({ colorGs: "rgba(0, 0, 0, 0.5)" })
        break;
      case "A":
        this.setState({ colorA: "rgba(1, 1, 1, 0.1)" })
        break;
      case "As":
        this.setState({ colorAs: "rgba(0, 0, 0, 0.5)" })
        break;
      case "B":
        this.setState({ colorB: "rgba(1, 1, 1, 0.1)" })
        break;
    }
    setTimeout( () => {
      this.sound[note].play(success => {
        if ( success ) {
          console.log("successfully finished playing.");
        } else {
          console.log("failed to play the sound.");
        }
      });
    }, 1);
  }
  stop( note ) {
    switch ( note ) {
      case "C":
        this.setState( { colorC: "white" } )
        break;
      case "Cs":
        this.setState( { colorCs: "black" } )
        break;
      case "D":
        this.setState( { colorD: "white" } )
        break;
      case "Ds":
        this.setState( { colorDs: "black" } )
        break;
      case "E":
        this.setState( { colorE: "white" } )
        break;
      case "F":
        this.setState( { colorF: "white" } )
        break;
      case "Fs":
        this.setState( { colorFs: "black" } )
        break;
      case "G":
        this.setState( { colorG: "white" } )
        break;
      case "Gs":
        this.setState( { colorGs: "black" } )
        break;
      case "A":
        this.setState( { colorA: "white" } )
        break;
      case "As":
        this.setState( { colorAs: "black" } )
        break;
      case "B":
        this.setState( { colorB: "white" } )
        break;
    }
    setTimeout( () => {
      for (let i=0; i<2000; i++) {
        this.sound[note].setVolume( 1.0-i/2000. );
      }
      this.sound[note].stop();
      this.sound[note].setVolume( 1.0 );
    }, 1 )
  }
  render () {
    return (
      <View style={styles.container}>
        <View style={{ flex: 1, flexDirection: "column", alignItems: "center" }}>
          <View style={{ flexDirection : "row", alignItems: "center", justifyContent: "center" }}>

            <View
              style={{ backgroundColor: "white", height: 100, width: 32, borderLeftWidth: 1, borderTopWidth: 1,}} >
            </View >
            <View
              onTouchStart={() => this.stroke("Cs")}
              onTouchEnd={() => this.stop("Cs")}
              style={{ backgroundColor: this.state.colorCs, height: 100, width: 32, borderTopWidth: 1, borderLeftWidth: 1,}} >
            </View >
            <View
              style={{ backgroundColor: "white", height: 100, width: 16, borderTopWidth: 1, }} >
            </View >
            <View
              onTouchStart={() => this.stroke("Ds")}
              onTouchEnd={() => this.stop("Ds")}
              style={{ backgroundColor: this.state.colorDs, height: 100, width: 32, borderTopWidth: 1, borderLeftWidth: 1,}} >
            </View >
            <View
              style={{ backgroundColor: "white", height: 100, width: 32, borderTopWidth: 1, }} >
            </View >
            <View
              style={{ backgroundColor: "white", height: 100, width: 32, borderTopWidth: 1, borderLeftWidth: 1, }} >
            </View >
            <View
              onTouchStart={() => this.stroke("Fs")}
              onTouchEnd={() => this.stop("Fs")}
              style={{ backgroundColor: this.state.colorFs, height: 100, width: 32, borderTopWidth: 1, }} >
            </View >
            <View
              style={{ backgroundColor: "white", height: 100, width: 16, borderTopWidth: 1, }} >
            </View >
            <View
              onTouchStart={() => this.stroke("Gs")}
              onTouchEnd={() => this.stop("Gs")}
              style={{ backgroundColor: this.state.colorGs, height: 100, width: 32, borderTopWidth: 1, }} >
            </View >
            <View
              style={{ backgroundColor: "white", height: 100, width: 16, borderTopWidth: 1, }} >
            </View >
            <View
              onTouchStart={() => this.stroke("As")}
              onTouchEnd={() => this.stop("As")}
              style={{ backgroundColor: this.state.colorAs, height: 100, width: 32, borderTopWidth: 1, }} >
            </View >
            <View
              style={{ backgroundColor: "white", height: 100, width: 32, borderRightWidth: 1, borderTopWidth: 1, }} >
            </View >

          </View>

          <View style={{ flexDirection : "row", alignItems: "center", justifyContent: "center" }}>

            <View
              onTouchStart={() => this.stroke("C")}
              onTouchEnd={() => this.stop("C")}
              style={{ backgroundColor: this.state.colorC, height: 100, width: 48, borderBottomWidth: 1, borderLeftWidth: 1 }} >
            </View >
            <View
              onTouchStart={() => this.stroke("D")}
              onTouchEnd={() => this.stop("D")}
              style={{ backgroundColor: this.state.colorD, height: 100, width: 48, borderBottomWidth: 1, borderLeftWidth: 1 }} >
            </View >
            <View
              onTouchStart={() => this.stroke("E")}
              onTouchEnd={() => this.stop("E")}
              style={{ backgroundColor: this.state.colorE, height: 100, width: 48, borderBottomWidth: 1, borderLeftWidth: 1 }} >
            </View >
            <View
              onTouchStart={() => this.stroke("F")}
              onTouchEnd={() => this.stop("F")}
              style={{ backgroundColor: this.state.colorF, height: 100, width: 48, borderBottomWidth: 1, borderLeftWidth: 1 }} >
            </View >
            <View
              onTouchStart={() => this.stroke("G")}
              onTouchEnd={() => this.stop("G")}
              style={{ backgroundColor: this.state.colorG, height: 100, width: 48, borderBottomWidth: 1, borderLeftWidth: 1 }} >
            </View >
            <View
              onTouchStart={() => this.stroke("A")}
              onTouchEnd={() => this.stop("A")}
              style={{ backgroundColor: this.state.colorA, height: 100, width: 48, borderBottomWidth: 1, borderLeftWidth: 1 }} >
            </View >
            <View
              onTouchStart={() => this.stroke("B")}
              onTouchEnd={() => this.stop("B")}
              style={{ backgroundColor: this.state.colorB, height: 100, width: 48, borderBottomWidth: 1, borderLeftWidth: 1, borderRightWidth: 1 }} >
            </View >

          </View>
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
    flexDirection: "row",
  },
});

鍵盤の表示

Viewで鍵盤を作ってきます。
鍵盤に触れた時に音が鳴り、離した時に音が鳴り終わるように、OnTouchStartとOnTouchEndに処理を書いていきます。
また、鍵盤に触れている時にbackgroundColorを変えたいので、stateにしておきます。

  constructor( props ){
    super( props );

    this.state = {
      colorC : "white",
      colorCs : "black",

      ...

    }

    ...

  }

  render() {

        ...

        <View
          onTouchStart={() => this.stroke("Cs")}
          onTouchEnd={() => this.stop("Cs")}
          style={{ backgroundColor: this.state.colorCs, height: 100, width: 32, borderTopWidth: 1, borderLeftWidth: 1,}} >
        </View >

        ....

  }

音を鳴らす(鍵盤に触れる)

コンストラクタで音源を読み込んでおきます。
鍵盤に触れた時、鍵盤のbackgroundColorをstateで変更して、音を鳴らします。
音が鳴らないことがあるため、setTimeoutが必要です。

  constructor( props ){
    super( props );

    ...

    this.sound = {};
    const soundList = [ "C", "Cs", "D", "Ds", "E", "F", "Fs", "G", "Gs", "A", "As", "B" ]
    soundList.forEach(note => {
      this.sound[note] = new Sound( note + ".mp3", Sound.MAIN_BUNDLE, error => {
        if ( error ) {
          console.log("failed to load the sound.", error);
        }
      })
    });
  }

  stroke ( note ) {
    switch ( note ) {
      case "C":
        this.setState({ colorC: "rgba(1, 1, 1, 0.1)" })
        break;

      ...

    }

    setTimeout( () => {
      this.sound[note].play(success => {
        if ( success ) {
          console.log("successfully finished playing.");
        } else {
          console.log("failed to play the sound.");
        }
      });
    }, 1);
  }

音を止める(鍵盤から指を離す)

同様にstateで鍵盤の色を元に戻します。
音をいきなり止めてしまうとブツッと鳴ってしまうので、徐々にボリュームを下げてから止めるようにします。
止めた後は、次に音を鳴らす時のためにボリュームを元に戻しておきます。

  stop( note ) {
    switch ( note ) {
      case "C":
        this.setState( { colorC: "white" } )
        break;

      ...

    }

    setTimeout( () => {
      for (let i=0; i<2000; i++) {
        this.sound[note].setVolume( 1.0-i/2000. );
      }
      this.sound[note].stop();
      this.sound[note].setVolume( 1.0 );
    }, 1 )
  }

まとめ

React Nativeでピアノを実装しました。
パフォーマンスは良いとは言えませんが、アプリにちょっとした鍵盤を実装したい場合(?)に試してみてください!

nabehide
Music, Programming Web, Native app, Generative art Nuxt.js, React.js, React Native, Unity
https://nabehide.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away