初心を思い出して好き勝手に開発した曲メモアプリ


そもそもクソアプリじゃないアプリとは?

サービスを開発する時にクソじゃないアプリにするために考えることと言えば下記のようなことになるかと思います。

  • 見た目がちゃんとしている
  • 致命的な不具合がほぼ無い
  • ちょっとした不具合も少ない
  • サービスを成すためにちゃんと必要な機能が整えられている
  • 利用規約やプライバシーポリシーがある
  • マネタイズ方法を考えている

今回のアプリの方針

でもクソじゃないアプリを作るというのは疲れます。後半のリリース前は利用規約や色々な説明文の調整など、やりたくないことばかりをやるハメになります。

ということで今回はクソアプリということなので何も気にせず好き勝手に作ろうと思い立ちました。元々アイデアとしてはあったけど、普段だったら絶対作らないだろうなぁ…というものをいい機会ですので作り進めてみました。具体的には下記のような方針です。

  • 見た目上変なところがあってもいい
  • 不具合の確認とかしないので最初の方に作った機能とか壊れている可能性がある
  • 作りたいところを優先的に作っているので使いづらくていも良い
  • マネタイズもアクセス解析も利用規約もなにもない

他者無視でとにかく自己中心的に作り進めていくだけのクソアプリです。

どういうアプリか

Web上で気軽に音を入力して再生できるアプリです。

ふと曲がコード進行含め頭に思い浮かぶことってよくありますよね。でもメモるのも面倒だしと思って放置していると100%忘れて二度と思い出せません。そんなときのためにWebで手軽に曲をメモしておけるツールです。記事を書き終わってToneEverybodyというわけのわからない名前でデプロイし終わってから思いましたが「キョクメモ」というサービス名だったら最高ですね! もしくは曲メモ帳。

リアルタイムでデータ保存

FirebaseのRealtime Databaseを使っており、データはどんどん保存されていきます。Realtime Databaseなので、他の人が編集したものもどんどんリアルタイムで共有されていきます。ですので例えばチャンネル1は自分が、チャンネル2は友達が入力、みたいにしてその場でジャムセッションみたいな感じで曲作りをしていくことも出来ます。

音を入力したら進む

メトロノームに合わせて音を正確に入力していくのはなかなか難しいため、一つ音を入力したら次の時間に進む、という形にしています。そのため楽器を弾くのに慣れていなくてもゆっくり一音ずつ正確に入力していくことが出来ます。

入力方式

PCの場合はキーボードの横並びであるz~m、r~pがドレミファソラシになっており、その上はピアノと同様シャープ音を利用することが出来ます。短いタイミングで押せば同じタイミングの和音にすることが出来ます。録音時は画面上にキーボードも出現するため、スマホの場合でも入力できます(この場合は同じチャンネル内の和音にすることは出来ません)。

バックスペースで戻る&削除、スペースキーで音を入力せずに次の時間に進むことが出来ます。スマホの場合も同様のボタンを用意しています。

再生の仕組み

入力後、もちろん実際に再生して音を鳴らして確認することが出来ます。元々ぼんやりとMIDIを使って再生することを考えていたのですが、よく使われているっぽいライブラリが大量にmp3ファイルを使っていて重すぎたりしてちょっと厳しいなぁ、と思っている時にTone.jsというフレームワークを発見しました。

Tone.js

Tone.jsはW3Cの草案であるWeb Audio APIというものを利用する形で作られています。そのため多分WebKitやFirefoxあたりでしか動作しませんが、スマホを含めたブラウザ上で動作するすごいやつです。Tone.jsの公式サイトにサンプルもたくさんありますので、この記事を読み終わった後にでも見てみてください。

具体的な再生方法

色々やり方があるのであくまで1例です。今回はチャンネルを利用したかったのでその形です。

まず再生のタイミングでPolySynthというものをチャンネルごとに作成します。これは音作りなどが出来る機能ですので一応チャンネル毎に音を変えることが出来ます(が今回は時間が足りず実装していません)。

    const synthes = music.channels.map(_channel =>
      new Tone.PolySynth().toMaster()
    )

その後、実際に音を入れていきます。再生のタイミングではなく、楽譜のように音を鳴らすタイミングを指定するだけです。triggerAttackReleaseは鍵盤を押して離す、という流れを指定できます。

      channel.playNotes.forEach(playNote => {
        Tone.Transport.schedule(function(_time) {
          synth.triggerAttackRelease(playNote.note.name, 0.5)
        }, playNote.time * 0.5)
      })

あとはstartすれば再生されます。

    Tone.Transport.start()

開発方法

今回はReactを使いました。React Hooksでちゃんと一から何かしらを作ったことがなかったので試してみました。ちなみにReact Hooksというのはクラスではなく関数コンポーネントを利用する方法です。ちゃちゃっと適当に開発しようと思ったのでReduxを入れていないのですが、やはりプロパティやコールバックを橋渡しするのが増えてきてしんどくなってきたため、だんだん後悔しています。

プロジェクトの作成方法

1ページの簡単なアプリを作るだけだったので、Next.jsなども使わずcreate-react-appで作成しています。今ってもうTypeScriptも含めて初期化出来るみたいで簡単でした。

npx create-react-app my-app --typescript

ハマったところ

キーボード入力

PCではキーボード入力を可能にしているため、コンポーネントが作成された最初のタイミングで document.addEventListener でキー入力イベントを設定する必要があります。クラスコンポーネントであればcomponentDidMount内でやればいいだけなのですが、関数の場合はそれがありません。そのためuseEffectという機能を利用する必要があります。

  useEffect(() => {
    document.addEventListener('keydown', onKeyDown)
    return () => document.removeEventListener('keydown', onKeyDown)
  })

再描画の時にreturnしている部分が呼び出されるため、ここでリスナーを破棄しています。

ただ、本来であれば下記のようにするべきです。

  useEffect(() => {
    document.addEventListener('keydown', onKeyDown)
    return () => document.removeEventListener('keydown', onKeyDown)
  }, [])

最後に空配列を入れています。これは、この中身が変わったらuseEffect内も生成し直してくれ、という指定です。空配列を指定することで中身が変わることがないため、ページが最初に表示された時以外は再生成されなくなります。

ではなぜそうせずに何度も生成し直しているのかというところですが、これは関数コンポーネントの基本的な仕組みが関係しています。

コンポーネント内の値は固定される

なんとなくuseStateを使ってステートを変更していると、簡単に値を変更できて楽だなぁというイメージがあるのではないかと思います。しかし実際にはそういうわけではなく、ステートが変わる毎に値の束縛が行われており、あくまでも再生成される時に最新の値で構築されているだけになります。

そのため普段は意識することがないかもしれませんが、今回のようにdocumentのキーボードイベントを取得したりすると、この場合だけ中のstateが全く変わっていないということがわかります。実際に、画面上の鍵盤で入力すると正しく音を登録できるのですが、キーボードで入力すると正常に音を登録できない、ということが発生しました。

そのため、ちょっと実装的にはアレかもですので他にやり方があるのかもしれませんが、キーボードイベントも毎回登録し直すような形にしています。

useRefというのもある

useRefというものがあり、ステートとは違い、参照を保存しておくことが出来るものです。

  const musicRef = useRef<Music>()

こんな感じで指定してchannels.currentに例えば今回はDOMの参照を入れておきます。

実は今回キーボードでは和音を入力できるようにするため、キー入力後にsetTimeoutを利用し、ちょっと時間が経って入力が無かったら次の時間軸に進む、ということを行っています。

これも先程のdocumentイベントのように曲者で、コンポーネント外から色々しようとするので正しくコンポーネント内の値を取得、更新できません。

そのため、useRefを使ってデータの参照を行って色々と処理してあげる必要があります。ちなみに、この場合createRefも正常に動作しないため、ちょっと面倒な手順を用意する必要もあったりします。1

このように、画面上のDOMからのイベント以外の色々なイベントを駆使しようとすると結構複雑になります。ハマりどころ且つ、処理が複雑になりきれいに書きにくくなると思いますので、可能であれば予め気をつけておいた方が良いような気がします。

まとめ

ということで完成させる気のないというクソアプリの紹介でした。下記はやろうと思っていたけど出来ていません。他に色々とやることがあるので多分もう作りません。

  • テンポを変える
  • 音色を変える
  • 一時的に倍速入力できるようにする
  • スマホでも1チャンネルに和音を入力できるようにする
  • 登録時に削除せずに戻るだけの機能
  • 再生時に超かっこいいアニメーション表示
  • 自分の曲をロックして他の人が編集できないようにする
  • 録音再開時はチャンネルの最後からスタート
  • mp3でダウンロード
  • 簡単タップで5000兆円手に入る機能

下記にアップしてありますのでぜひ遊んでみてください。使い方の説明ももちろんありませんので雰囲気でどうぞ! ちなみに誰かがもうぐちゃぐちゃにしちゃっていてもう無いかもしれませんがmayという昔作ったノスタルジックな曲をサンプルでちょっとだけ入れています。

Tone Everybody
https://tone-everybody.netlify.com/

ちょっとでも良いなと思うところがあったら是非いいねをお願いします!

(曲を登録したら続きを作りたくなってしまう不思議……)