もしもピアノが弾けたなら【Go言語編】

クリスマスまで残すところあと10日ほどとなりました。
本日の担当はアイスタイルR&D部のgrassyです。こんにちは。

突然ですが皆様、クリスマスの予定は決まってますか?
「可愛いあの子をデートに誘いたい」「5年付き合った彼女にプロポーズしたい」はたまた「面倒な彼氏と縁を切りたい」などなど。きっと、胸の内に様々な目標を立てていることでしょう。
今日は、そんな目標を達成するのに役立つ思考法を、『ピアノを自由に弾ける(鳴らせる)ようになるまで』という例に沿って、Go言語でCLIツールを作りながら、ゆる〜いストーリー仕立てでお届けします。
全体的にゆる〜い記事ですので、ジンジャーブレッドとコーヒー片手にのんびり気分でお楽しみ下さい〜

ストーリー

ステップ1・課題(作りたいもの、やりたいこと、出来ていないこと)を明確にする

今回の私の第一の目標は『何か面白いものを作りたいなぁ』でした。この時点では大分曖昧でモヤッとしてますね。スタートなんてそんなものです。
さて、ここからこのモヤッとした日本語を噛み砕いていきます。
『面白いもの』とは何でしょう?面白いの定義は?
誰にとって面白いの?ものって結局何なの?物体なの?概念なの?ツールなの?
・・・うーん、日本語ムズカシイ。

なので、困った時はそんな浮かんだ疑問を書き出します。

question answer
誰にとって? 私個人にとって
面白いって何? 夢中になれるもの
何が面白いの? 音楽(趣味で楽器をやってます)
ものって何? 音楽に関するものとしたら、物体orツール

せっかくなら技術的な話も絡めたい! (←これはこの段階では雑念です)

よ〜し!キーボードを使って音がなるツールを作ってみよう!
こうして私の課題は『キーボード入力を受け取って音声を出力するツールを作りたい』であることがはっきりしました。

このステップは思考プロセスにおいて 課題の認識 にあたります。

ステップ2・課題解決のために既にあるもの、不足しているものはなにか

課題を認識してようやくスタートラインです。
次ははじめの一歩として、作りたいものを作るため(=課題解決)に必要なものは何かを書き出します。

例の場合は、大きく分けて

  • キーボード入力を受け取る手段
  • 音声を出力する手段

となります。

では、その中で現時点で既にあるもの(解決済みの問題、解決方法が明白の問題)、不足しているもの(解決方法が未知の問題)は何かを明確にさせていきます。

キーボード入力を受け取る手段をどうするか?

この問題の解決方法はいろいろあります。あるゆる言語、あらゆるツール、あらゆる手段で解決できます。
ただ、個人的に業務ではGo言語に触れる機会が多く、既に持っている知識でこの問題が簡単に解決可能であることを私は知っています。
つまりこの問題は私の中で、【既にあるもの】に分類されます。

音声を出力する手段をどうするか?

この問題にもいろいろな解決方法がありそうです。
音声ファイルと聞いて一番に思いつくものは何でしょうか?MP3?WAV?AAC?WMAなんてものもありますね。
さて、ここで今解決したい課題は何だったのかを思い出しましょう。そうです、あくまで音声が出力することなのです。つまり、生音であることよりもデータとして扱いやすいことが優先されます。
データで音を鳴らす・・・それなら、適しているのはMIDIなんじゃん?と私は行き着きました(今更MIDI!?って思った方はお口チャックをお願いします!)
ただ、私はMIDIについては詳しくありません。この問題は私の中で【不足しているもの】に分類されます。

よ〜し!これで不足がはっきりしたぞ!基礎知識から勉強しなきゃ!

このステップは思考プロセスにおいて現状把握・問題分析にあたります。

ステップ3・解決策立案

今の状態で明確となっている私の課題は、MIDIを出力する方法がまるで分からないことです。
私にはMIDIの基礎知識が全くありません。正直、MIDIがデータの集合体によっていい感じに音を鳴らしているという程度のことしか知りません。
そこで、段階を追って課題を解決していきます。

MIDIの話が続くので、興味がなければ読み飛ばし可

①まずはMIDIの基礎についてを勉強します

土台がなければ解決も何も出来ません。ということで、何を新しく始めるにもまずは勉強です。

MIDIとは一体何者なのか?

MIDI規格上のデータの送受信は、すべてMIDIメッセージで行われる。
MIDIメッセージを効率よく送信するために、MIDIメッセージに使用されるバイトは「ステータスバイト」か「データバイト」の大きく2種類に分けられる。これらの先頭は常にステータスバイトで始まり、ステータスバイトの後に任意の個数のデータバイトが続く。
ステータスバイトでは、ノートオンやコントロールチェンジ、システムエクスクルーシブなどを定義する。
データバイトは、ステータスバイトで定義したものについて、その内容や数値を指定するのに使用する。
@wikipedia

ふむふむ。
「MIDIがデータの集合体によって何かいい感じに音を鳴らしている」という認識は、それほど大きく間違っていなかったことがわかりますね。
では、そのデータはどういう形式で書かれてるのでしょうか?
もう少し調べてみると、デファクトスタンダードとなっているフォーマットがあるらしいことに辿り着きました。

じゃあ次はそれを調べましょう。

MIDIのデファクトスタンダードフォーマット、SMFとは?

スタンダードMIDIファイル(Standard MIDI File、SMF)は、MIDI用ファイルフォーマットの一つである。SMFはチャンクと呼ばれるデータブロックから構成される。ファイルの先頭にあるチャンクはヘッダチャンク、それに続く演奏データが入るチャンクはトラックチャンクと呼ばれる。
@wikipedia

ふむふむ。
MIDIがどんなデータの集合体なのかはっきりしてきましたね。
初期段階でいじりたいのはメロディーデータぐらいなので、どうやら必要なデータはトラックチャンクにあるようです。このトラックチャンクの中には、イベント実行時のタイミングも、音程も、どんなイベントなのか(音を鳴らすのか止めるのか)も、メロディーを形成するために必要な情報が全て含まれます。

よ〜し!ここまで理解すれば音階程度のデータは完璧!完璧なんです!完璧なんだってば!

・・・ということで、ここまでの話をざっくりまとめます。
例えば「1チャンネルのC4(60=3CH/ピアノでいう真ん中のド)をベロシティ(音量)90で鳴らす」というイベントは16進数で 91H(チャンネルの指定) 3CH(音階の指定) 5AH(音量の指定) という表現がされます。

ついつい調べることが楽しくて本題から大きく道が逸れてしまいましたが、最終的にMIDIを出力するためには上記データをバイナリに出力できれば良いことがわかりました。

やったー。

②それで、肝心のバイナリデータを出力する手段は?

往々にして1つの課題を解決すると新たに次の課題が現れるものです。助けても助けても拐われるピ○チ姫のようなものです。

MIDIのSMFについてを理解したところで、データを作ってサクッとバイナリ化出来るわけではありません。
もちろんこれについても自力で解決していく方法もたくさんあるでしょう。ただ、今回の私の課題は『MIDIを出力すること』ではありません。本質を見失ってはいけません。(手を抜けるところは手を抜きたいですし)
ですので、もともと同じようなことをやっている人はないか、使えるライブラリはないかを調べてみます。

・・・ありました → https://github.com/algoGuy/EasyMIDI

見事にSMFデータをMIDI出力してくれるGoのパッケージです。ありがたくこちらを使わせていただきましょう。

MIDIの話、おしまい

さて、これでようやく全ての課題について解決策の目星が付きました。

これが解決策立案プロセスです。

いよいよ残されたプロセスは、お待ちかねの実践です。

ステップ4・実践(立案した解決案の実施)

というわけで、実際に作ってみました。

準備するもの

Goの実行環境

やったこと

前準備

コマンド実行用にCLIパッケージも追加しておきましょう。
https://github.com/urfave/cli をお借りして、予め簡単にCLIツールとして実行できるようにします。

$ go run main.go input-score -o=”${OUTPUT_FILE_NAME}”

この記事では上記の形式で該当のコマンドを実行出来るようにしています。

1.CLI上でキーボード入力を受け付ける(パイプ受け取りも可能とする)
    var str string
    /*
    Go標準パッケージのterminalが持つIsTerminal関数は
    インタラクティブに入力を受けるかの判定を行う
    */
    if terminal.IsTerminal(0) {
        // trueであればキーボードからの入力を待つ
        // spaceの入力を許容するためfmt.Scanは使わない
        scanner := bufio.NewScanner(os.Stdin)
        scanner.Scan()
        str = scanner.Text()
    } else {
        // falseであればパイプラインで渡された文字列を受け取る
        // Sample (Twinkle Twinkle Little Star): echo "aagghhg ffddssa ggffdds ggffdds aagghhg ffddssa " | go run main.go input-score -o=star
        stby, _ := ioutil.ReadAll(os.Stdin)
        str = string(stby)
    }
    fmt.Printf("input: %s", str)
2.キーと音階のマッパーを作る

どのキーが入力されたらどの音を鳴らすかのマッパーを作ります。私はキーボードをピアノに見立てて、下図のようにマッピングしました。

音階とキーボードのキーマップ

const (
    Rest uint8 = 0
    C          = iota + 0x3c
    Cis
    D
    Dis
    E
    F
    Fis
    G
    Gis
    A
    B
    H
    HighC
    HighCis
    HighD
    HighDis
    HighE
)

func Tone(key string) uint8 {
    list := map[string]uint8{
        "a": C,
        "w": Cis,
        "s": D,
        "e": Dis,
        "d": E,
        "f": F,
        "t": Fis,
        "g": G,
        "y": Gis,
        "h": A,
        "u": B,
        "j": H,
        "k": HighC,
        "o": HighCis,
        "l": HighD,
        "p": HighDis,
        ";": HighE,
        " ": Rest,
    }
    return list[key]
}

Cがピアノでいうドの音になり、そこから半音ずつ上がってます。こういう時にiotaはとても便利ですね。

3.入力に合わせたトラックチャンクを作成

ライブラリのサンプルを参考にして、トラックチャンクを追加します。

    var score []uint8
    for _, t := range str {
        // 入力された文字列からkeymapの割当て
        score = append(score, tonemap.Tone(fmt.Sprintf("%c", t)))
    }

    division, err := smf.NewDivision(ticks, smf.NOSMTPE)
    if err != nil {
        return err
    }
    midi, err := smf.NewSMF(smf.Format0, *division)
    if err != nil {
        return err
    }
    track := &smf.Track{}
    err = midi.AddTrack(track)
    if err != nil {
        return err
    }
    var list []*smf.MIDIEvent

    // マップから作成されたscoreデータをイベントリストに追加していく
    for i, t := range score {
        var d uint32
        if i != 0 {
            d = onDeltaTime
        }
        // トーンオン(音を鳴らすイベント)
        toneOn, err := smf.NewMIDIEvent(d, smf.NoteOnStatus, 0x00, t, 0x64)
        if err != nil {
            return err
        }
        list = append(list, toneOn)
        // トーンオフ(音を消すイベント)は固定
        toneOff, err := smf.NewMIDIEvent(offDeltaTime, smf.NoteOffStatus, 0x00, t, 0x64)
        if err != nil {
            return err
        }
        list = append(list, toneOff)
    }

    for _, l := range list {
        if err := track.AddEvent(l); err != nil {
            return err
        }
    }
4.足りないデータと合体させて出力!

上記説明のとおりMIDIデータにはヘッダチャンクが必要だったり、メタデータが必要だったりします。そういった不足を合体させて、サンプル通りに出力します。

    // トラック終了イベントの追加
    metaEvent, err := smf.NewMetaEvent(21, smf.MetaEndOfTrack, []byte{})
    if err != nil {
        return err
    }
    if err := track.AddEvent(metaEvent); err != nil {
        return err
    }

    // アウトプットするファイル名、パスを指定してCreate
    output, err := os.Create(fmt.Sprintf("data/%s.mid", file))
    if err != nil {
        return err
    }
    defer output.Close()

    writer := bufio.NewWriter(output)
    smfio.Write(writer, midi)
    if err := writer.Flush(); err != nil {
        return err
    }

ここまでが今回の実装内容のほぼ全容となります。

全ての実装が終わったら下記コマンドを実行してみます。

$ echo “asdfghjk” | go run main.go input-score -o=onkai

onkai.mid が出力されていたら成功です!

ステップ5・評価/改善

本来であれば、実践は一回では終わらないことが多いでしょう。
一度実践することで更に新たな課題が生まれ、それを解決していくステップが必要となります。これはPDCAサイクルのCheck/Action段階に該当します。

・・・が、思ったよりネタになりそうな大きな課題が浮かばなかったので、ここでは小ネタ的な課題でお茶を濁します。

実践中に生じた(しょうもない)課題

MIDI出力できたぞー!
→さっそく鳴らしてみるぞー!
→・・・あれ?そもそも今どきのmacOSXってMIDIの再生環境ないんじゃ(´・ω・`)<-GarageBand削除した奴

というわけで。

もちろん、MIDIが再生できるプレイヤーアプリケーションを落としてもいいのですが、開発しながらだとコマンドラインで叩けたほうが便利、データが見れたらなお便利、と考えて下記ツールをインストールしました。

timidity

http://twsynth.osdn.jp/

インストール
$ brew install timidity
使い方
$ timidity onkai.mid

これで、音を鳴らす術を手に入れました!完全勝利!

再評価

それでは気を取り直して、取得したMIDIを実際に鳴らしてみましょう。(WordPressでMIDIを直接鳴らせなかったのでMP3にコンバートさせてます)

ちゃんとドレミファソラシドって鳴りましたか?鳴りましたね?

コード中にあるSampleを実行するとこうなります。

耳慣れたメロディーが奏でられましたね?
これでもう自由にピアノが弾けるようになったも同然です。

実物

こちらが実際に実装してみた内容になります。

https://github.com/grassy-48/midipia

お手元にGoの環境をお持ちの方はぜひ遊んでみてください。

こうして、私は無事に最初の課題であった『キーボード入力を受け取って音声を出力するツールを作りたい』を実現させたのでした。
めでたしめでたし。

おわりに

もうちょっとだけ続きます。
この記事では、目標を達成するまでに個人的に気をつけている思考プロセスについてを『ピアノを自由に弾ける(鳴らせる)ようになるまで』という例に沿って書きました。
最後にこのプロセスについてを『可愛いあの子(仮称・Aさん)をデートに誘いたい』という例で振り返ってみましょう。

  • ステップ1・課題(作りたいもの、やりたいこと、出来ていないこと)を明確にする
    • 課題:『Aさんをクリスマスデートに誘う』
  • ステップ2・課題解決のために既にあるもの、不足しているものはなにか
    • 既にあるもの:Aさんの連絡先
    • 不足しているもの:勇気、Aさんの予定の把握、デートプランの作成
  • ステップ3・解決策立案
    • Aさんの予定の把握→Aさんに予定を確認する
    • デートプランの作成→友人に聞く、雑誌を読む、ネットで調べる、etc.
  • ステップ4・実践(立案した解決案の実施)
    • Aさんに予定を確認する
  • ステップ5・評価/改善
    • 返事がOKであれば→デートプランの提示
    • 返事がNOであれば→ドンマイ☆他の予定を立ててクリスマスを楽しもう!

例に用いたプログラミング開発に限らず、こういった思考法は普段の生活に役立つ場面も少なくありません。
意識せずに出来ている方や、当たり前に実践している方も多いでしょう。
でも、あなたがもしそうでないのなら、まずは意識するとこから初めてみてはいかがでしょうか。
ほんの一匙意識を変えるだけで、もしかしたらクリスマスに可愛い彼女(彼氏)が出来るかもしれませんよ!

最後の最後ですが、上記ツールを実際に試してくれた方へのクリスマスプレゼントです。ぜひお手元の環境で下記コマンドを実行してみてください。

$ echo "hhh hhh hkfgh  uuu hhh hgghg k hhh hhh hkfgh  uuu hhh kkugf" | go run main.go input-score -o present

それではみなさま、よいクリスマスをお過ごしください!
メリークリスマス!!

参考

使用パッケージ

サーバーサイドエンジニア。主な使用言語はGoとPHP。機会があればフロントもやりたい。なんでもやりたい。某カエルのキャラクターをこよなく愛する。