5.6 要素の調整と融合
特に新しいことはやっていない。今まで作ってきた要素を調整し、融合して1つのものとした。
譜面の調整
バックに音程の基準を表す土台をつけた。左端に音程も示している。黒鍵音は黒っぽいバックにしていたりと何気に凝っている。
譜面はGUITextureを使って表示していたが、そもそもGUI系は画面上に表示するゲージとかに使われるもので、最も上に描画されてしまう。今回の場合、それは非常に困るので、新しいGameObject(Plane)を生成してテクスチャを貼り付ける作戦に出た。
譜面をめくる度に、以下の処理を行う。
- ノートを全て消去する(GameObjectをDestroyする)
- 現在の譜面で必要なノートを計算する
- 新たなGameObjectを生成する
しかし、この方法だと譜面をめくる度に0.3秒ほどの処理落ちが発生した。どうやらGameObjectの生成・削除は少し重い処理らしい。
よって、以下の方法に変更した。
- 初期化時にノートを全て計算し、GameObjectを生成する
- 譜面をめくる度に、全てのノートを非表示にする(SetActive(false))。また、現在の譜面で必要なノートを計算し表示を有効にする
これでもやや処理落ちが発生するものの、全然マシだったので採用。
あと苦労したのは、ノートの表示位置の基準。以下のアルゴリズムで最高点の音程を決定している。
- 表示している譜面の平均音程を中央に持ってくるように最高点の音程を決定する
- 表示している譜面の最高音程が表示できていない場合、最高点の音程をその音程とする
- 2以外の場合で表示している譜面の最低音程が表示できていない場合、最低点の音程をその音程とする
…だけど、どうやらどこかにバグが有るらしく、たまに上手く動かない(妙に上に表示される)。まぁ、ぼちぼちと修正していこう。
演奏位置の表示
赤線のこと。
Unityには再生中のオーディオの再生位置を手に入れる機能が標準で搭載されている。
public AudioSource obj; void Start(){ obj.Play(); } void Update(){ Debug.Log(obj.time); // 再生位置がfloat型・秒数で取得できる }
なお、数値を弄って再びPlayすることで指定位置から再生も可能。
これと全ての要素を同期させている。
歌唱線の表示
音が取れていない場所でも線が繋がるのは不格好なので、いくつかLineRendererを用意して…と考えていたが、非常に面倒なのでパスした。まぁ、機能上そこまで影響ないやろ…。
ノートがない場所では譜面中に収まるオクターブに、ある場所ではノートとの差が最も小さくなるオクターブに合わせて表示する。JOYでよく発生するオクターブエラーはこれで回避できる。
16:9表示への対応
カメラサイズを適当にしていたので、全画面表示で左右が切れてしまった。
今の時代だと16:9のモニターが標準だと思うので、それに合わせて作りなおした。
カエルの歌を歌った。
5.5 MIDIの解析
やりたいことをやってくれるライブラリが見つからなかったので、今回はとても苦労しました。
Standard MIDI Format
MIDIは、音楽ファイルの形式の一つである。
.wavや.mp3は音波データそのものを記録しているため、再現性が高い代わりにデータが大きく編集が困難である。対照的に、.midは楽譜データを記録しているため、再現性が低い(音源依存)代わりにデータが小さく編集が容易である。
そんなMIDIの標準的な規格が、Standart MIDI Format(SMF)である。
SMFは、ヘッダ部分とトラックデータ部分に分かれている。
たとえば、以下のようなMIDIファイルを考える。これはDominoという有名なエディタである。
見えていないが、第1トラックでテンポを120に設定している。また、見えている第2トラックでは四分音符でド、四分音符でレ、二分音符でミを鳴らし、一小節で終わる。
これをフォーマット1(後述)でSMF(Standard MIDI File)に変換し、バイナリエディタで開いてみる。
今回は、このデータを解析していく。このデータは、ヘッダ+トラックごとのデータで構成されている。
ヘッダ
4D 54 68 64 | 00 00 00 06 | ** ** | ** ** | ** ** |
"MThd" | 実データの大きさ(必ず6) | フォーマット(0000/0001/0002) | トラック数 | 時間単位 |
ヘッダは以上の14bytesからなる。
フォーマット
フォーマット0は、全てのトラック(パート)を1トラックに押し込めたデータである。プレイヤーが譜面を起こすのが楽になる反面、編集データからの不可逆的な変換のため編集が困難になる。
フォーマット1は、トラックごとに譜面データを記録している。こちらは編集データから可逆的な変換である。
フォーマット2は、まず使われることがないらしいので省略。
トラック数
パートの数。
時間単位
正の数(先頭ビットが0)の場合、デルタタイム(後述)において、四分音符1個分を表す数値。分解能のことだと思えば良い。
負の数(先頭ビットが1)の場合は違う意味を表すが、まぁ、省略。
これを基に、画像のデータを解析してみよう。
- フォーマット … 00 01 -> 1
- トラック数 … 00 02 -> 2
- 時間単位 … 01 E0 -> 480
実データ
ヘッダの後に、トラックごとのデータが続く。たとえば、トラック2は以下の部分である*1。
それぞれのトラックについて、以下の様な構造になっている。
4D 54 72 6B | ** ** ** ** | ** ** ~ |
"MTrk" | 実データのデータ長 | 実データが続く |
すなわち、"MTrk"で始まった後4bytesで表されたデータ長があり、実データが続く。
実データは以下の様な構造になっている。ここがややこしい部分である。
** ~ | ** | **~ |
デルタタイム | ステータスバイト | 諸々 |
デルタタイム
以降続く命令を実行するまでに待機する時間。RoboLabを触ったことがある人は、アレのウェイトと同じようなものだと考えれば良い。ヘッダで設定した時間単位を基準として記述する。
ところで、デルタタイムは何byteあれば十分なのだろうか。SMFでは、デルタタイムは4bytesまで設定可能となっている。ところが、大抵の命令のデルタタイムはもっと少ないbyteで収まることは容易に想像できると思う。
そこで、SMFでは可変長形式でデルタタイムが記述されている。これがめんどくさい。
可変長形式
理屈は単純で、先頭bitが1の時は続くbyteもデルタタイムを表し、0の時はそこがデルタタイム表現の終点であるだけである。これを実際のbit列に変換するには、先頭ビットを取っ払って右に詰めれば良い。
たとえば、以下の様になっている。接続判断bitを赤色で示す。また、見やすいように2進数表記は1byteずつ縦線で区切った。
16進数 | 2進数 | 可変長2進数 | 可変長16進数 |
40 | 0100 0000 | 0100 0000 | 40 |
7F | 0111 1111 | 0111 1111 | 7F |
80 | 1000 0000 | 1000 0001 | 0000 0000 | 8100 |
200000 | 0010 0000 | 0000 0000 | 0000 0000 | 1000 0001 | 1000 0000 | 1000 0000 | 0000 0000 | 81 80 80 00 |
要点は、今見ているアドレスをindexとすると先頭ビットが1かどうか判断するには以下のようなコードを書けば良いということである。
if((index & (byte)0x80) > 0){ // byte型のindexは先頭ビットが1 }
ステータスバイト
1byteで、命令の種類を表す。以下に、カラオケ採点上必要な項目のみ述べる。nは0~Fまでの数値で、対象チャンネルを表す。また、ノートオン/オフの場合、ステータスバイトの後の1byteは対象音階を表し、その後の1byteはベロシティ(強さ)を表す。
** | 意味 |
8n | ノートオフ |
9n | ノートオン |
(A~E)n | ほげほげ |
F0またはF7 | ふがふが*2 |
FF | メタイベント(後述) |
ランニングステータス
上で述べた通り、ステータスバイトは「(8~F)* **」の形式であり、すなわち先頭ビットは必ず1である。ここで、ステータスバイトの先頭ビットが1でない場合、前回のステータスバイトを受け継ぐ(すなわち、ステータスバイトを省略する)というデータ圧縮が可能である。これをランニングステータスと呼ぶ*3。
メタイベント
色々とオプション的なイベントである。カラオケ採点においてはテンポ変更とトラック終端くらいしか重要なものがないので、そこを中心に述べる。
メタイベントは以下の様な形式である。
ステータスバイト | メタイベント種類 | データ長(可変長形式) | データ | 意味 |
FF | 51 | 03 | ** ** ** | テンポ変更 |
FF | 2F | 00 | ---- | トラック終端 |
メタイベントは必ず上のような形式になっているため、カラオケ採点の場合は読み込み時、テンポ変更以外のメタイベントに当たった場合、データ長を把握してその分スキップする処理が重要である。
テンポ変更について。
データは3bytesである。この数値は四分音符のマイクロ秒を表す。つまり、BPM(1分あたりの四分音符の数)をBとすると、となる。
以上を把握した上で、先ほどのトラック2の実データ部分(54byte目から)を読んでみよう。
分かりやすくするため、デルタタイムは赤色、ステータスバイトは黄色、メタイベントは水色で表す。
イベント | 意味 |
00 FF 03 00 | デルタタイム0後にメタイベント03を実行、データ長は0(つまり、無い) |
00 FF 21 01 00 | デルタタイム0後にメタイベント21を実行、データ長は1でデータは0 |
00 90 30 64 | デルタタイム0後にチャンネル0の音階30をベロシティ64でON |
83 60 80 30 00 | デルタタイム83 60(可変長形式)後にチャンネル0の音階30をOFF |
00 90 32 64 | デルタタイム0後にチャンネル0の音階32をベロシティ64でON |
83 60 80 32 00 | デルタタイム83 60(可変長形式)後にチャンネル0の音階32をOFF |
00 90 34 64 | デルタタイム0後にチャンネル0の音階34をベロシティ64でON |
87 40 80 34 00 | デルタタイム87 40(可変長形式)後にチャンネル0の音階34をOFF |
00 FF 2F 00 | トラック終端 |
つまり、音階30はド、音階32はレ、音階34はミということである。
フォーマット1のデータ読み込み
フォーマット1の場合、トラックごとに変数を作り、並列的に読んでいくしか無い。
現在の時間から最も短い時間でイベントにたどり着けるのがどのトラックか把握し、それを再帰的に処理すれば良い。
カラオケ採点のために
カラオケ採点のためには、「どのようなイベントが」「どのくらいの時間(秒)」継続するか知ることが大切である。
そこで、イベントを「何もしない」「ノートhogeをON」「テンポ変更」「トラック終端」の4種類に分けてそれに継続時間を付属させた。
以下、SMFLibraryのソースである。投げやりな感じで貼り付けるが、上の説明をよく読めば理解できるはずだし、私も相当苦労したのでこれで勘弁して欲しい。ソース中にはコメントを大量に付けたので、それを読めばある程度理解できるはずである。
using UnityEngine; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace SMFLibrary { // Standard Midi Format のカラオケ用読み込みに特化したライブラリ public class StandardMidiFormat { // イベントリスト用 public class Event { // 0:何もしない 1:BPM変更 2:ノートON -1:EOF public int type; // イベント内容。タイプに応じて意味が違う // 1:BPM 2:ONにするノート 0,-1:未定義 // ノートはA0(55Hz)が0になり、半音上がるごとに1上がる public int value; // イベントの継続時間(ms) public int time; } // 現在読み込んでいるファイルの名前 public string filename { get; private set; } // 現在読み込んでいるMIDIのイベントリスト // ただし、BPM変更とノートON/OFF(1音)のみ public List<Event> event_list; // 4分音符に当たるデルタタイム private int time_unit; // テンポ(四分音符に当たるms) private int tempo; // byte列からshort型整数に変換する private int ToInt16(byte[] data, int index) { byte[] ar = new byte[2]; ar[0] = data[index]; ar[1] = data[index + 1]; if (BitConverter.IsLittleEndian) { Array.Reverse(ar); } return BitConverter.ToInt16(ar, 0); } // byte列からint型整数に変換する private int ToInt32(byte[] data, int index) { byte[] ar = new byte[4]; ar[0] = data[index]; ar[1] = data[index + 1]; ar[2] = data[index + 2]; ar[3] = data[index + 3]; if (BitConverter.IsLittleEndian) { Array.Reverse(ar); } return BitConverter.ToInt32(ar, 0); } // bit列からint型整数に変換する private int ToInt32FromBits(byte[] data, int index) { int value = 0; for (int i = 0; i < 32; i++) { value += data[index + 31 - i] * (int)Math.Pow(2, i); } return value; } // バイナリファイルを読み込み、配列で返す private byte[] _load_binary(string filename) { System.IO.FileStream fs = new System.IO.FileStream(@filename, System.IO.FileMode.Open, System.IO.FileAccess.Read); int filesize = (int)fs.Length; byte[] data = new byte[filesize]; fs.Read(data, 0, filesize); fs.Dispose(); return data; } // dataのindexから始まる可変長形式の数値をint型に変換する // plus_posは、可変長形式の数値がそこから何byteあったか private int _convert_variable_to_int(byte[] data, int index, out int plus_pos) { // 可変長形式の最大byte int max_variable_size = 5; // 変換後の最大byte int max_int_size = 4; // データサイズ int size = 1; for(int i = 0; (data[index+i] & (byte)0x80) > 0; i++) { size++; } if (size > max_variable_size) { Debug.LogError("variable size is bigger than 5"); plus_pos = 0; return 0; } // 変換後の各ビットデータ byte[] bits = new byte[max_int_size*8]; // 埋めるべきビット int pos = bits.Length - 1; // ビットデータを埋める for (int i = 0; i < size; i++) { for(int j = 0; j < 7; j++) { byte comp = (byte)(1 << j); if((data[index+size-1-i] & comp) > 0) bits[pos] = 1; pos--; } } // 埋まったビットデータからintに変換して返す plus_pos = size; return ToInt32FromBits(bits, 0); } // デルタタイムを実際の時間(ms)に変換する private int _convert_delta_time_to_ms(int delta_time) { return (int)((float)tempo * (float)delta_time / (float)time_unit); } // data内の第nトラックがindex[n-1]から始まるとして、最も早く実行される命令を返す // before_commandはトラックの直前ステータスバイト、timeはトラックの現在時間 // indexとtimeの中身は変更される。indexとtimeが-1になったら、そのトラックは終了した // どのトラックも終了していたらnullを返す private Event _get_command(byte[] data, int[] index, byte[] before_command, int[] time) { // 現在の全体の時間 int world_time = time.Max(); // 最小の(world_timeからの)デルタタイム・最小デルタタイムのインデックスナンバー・動かすインデックス座標 int min_delta_time = -1; int min_index = -1; int min_plus_pos = -1; for (int i = 0; i < index.Length; i++) { if (index[i] >= 0) { int plus_pos; int delta_time = time[i] + _convert_variable_to_int(data, index[i], out plus_pos) - world_time; if (min_delta_time < 0 || delta_time < min_delta_time) { min_delta_time = delta_time; min_index = i; min_plus_pos = plus_pos; } } } // 次に実行されるトラックが分かったから、その内容を読んでindexと時間を動かす if (min_index >= 0) { index[min_index] += min_plus_pos; time[min_index] += min_delta_time; // ステータスバイト byte status_byte = data[index[min_index]]; Event ev = new Event(); // ステータスバイトの最上位ビットが1なら、具体的命令 if ((status_byte & (byte)0x80) > 0) { index[min_index]++; before_command[min_index] = status_byte; } // ステータスバイトの最上位ビットが0なら、前回の命令 else { status_byte = before_command[min_index]; } byte st = (byte)(status_byte & (byte)0xF0); if (st == (byte)0x80) ev.type = 0; // ノートオフ else if (st == (byte)0x90) ev.type = 2; // ノートオン else if (status_byte == (byte)0xFF && data[index[min_index]] == (byte)0x51) ev.type = 1; // テンポ変更 else if (status_byte == (byte)0xFF && data[index[min_index]] == (byte)0x2F) ev.type = -1; // トラック終端 else // 無効命令 { if (st == (byte)0xA0 || st == (byte)0xE0) { index[min_index] += 2; } else if (st == (byte)0xC0 || st == (byte)0xD0) { index[min_index] += 1; } else if (st == (byte)0xB0) { if (data[index[min_index]] == (byte)0x7E && data[index[min_index] + 2] == (byte)0x04) { index[min_index] += 3; } else { index[min_index] += 2; } } else { int plus_pos; int length = _convert_variable_to_int(data, index[min_index] + 1, out plus_pos); index[min_index] += 1 + plus_pos + length; } return _get_command(data, index, before_command, time); } // ノートオフかオンかテンポ変更の命令だけ抽出したので、諸々の処理 // 次のデルタタイムが記録されているインデックスナンバー int next_delta_time_index = index[min_index]; // 何もしない処理 if (ev.type == 0) { next_delta_time_index += 2; index[min_index] += 2; } // BPM変更の処理 else if (ev.type == 1) { next_delta_time_index += 5; ev.value = 0; ev.value += (int)((uint)data[index[min_index] + 2] * 65536); ev.value += (int)(uint)(data[index[min_index] + 3] * 256); ev.value += (int)(uint)(data[index[min_index] + 3]); ev.value /= 1000; tempo = ev.value; index[min_index] += 5; } // ノートオンの処理 else if (ev.type == 2) { next_delta_time_index += 2; ev.value = (int)data[index[min_index]]; index[min_index] += 2; } // トラック終端の処理 else if (ev.type == -1) { index[min_index] = -1; time[min_index] = -1; } int pp; // デルタタイムを実際の時間(ms)に変換して代入 ev.time = _convert_delta_time_to_ms( _convert_variable_to_int(data, next_delta_time_index, out pp) ); return ev; } return null; } // ファイルを読み込み、イベントリストを得る public void Load(string filename) { byte[] data = _load_binary(filename); event_list = new List<Event>(); tempo = 500; // BPM120 // ヘッダを読み込む if (data.Length < 4) { Debug.LogError("File Length is not suitable."); } else if (!(data[0] == (byte)'M' && data[1] == (byte)'T' && data[2] == (byte)'h' && data[3] == (byte)'d')) { Debug.LogError("File is not Standard MIDI Format."); } else if (data[9] == 2) { Debug.LogError("Format 2 is not suitable."); } else { // このMIDIのフォーマット int format = ToInt16(data, 8); // このMIDIのトラック数 int track_num = ToInt16(data, 10); // 4分音符に当たるデルタタイム time_unit = ToInt16(data, 12); if (format == 2) { Debug.LogError("format is 2."); } else if (time_unit < 0) { Debug.LogError("time_unit is minus."); } else { // 読み込んでいるインデックス(トラックごと) int[] pos = new int[track_num]; pos[0] = 22; for (int i = 1; i < track_num; i++) { pos[i] = pos[i - 1] + ToInt32(data, pos[i - 1] - 4) + 8; } // 各トラックの現在の時間 int[] time = new int[track_num]; // 各トラックの直前命令 byte[] before_command = new byte[track_num]; // 「何もしない」をイベントリストの先頭に追加 int min_time = -1; for (int i = 0; i < track_num; i++) { int plus_pos; int t = _convert_variable_to_int(data, pos[i], out plus_pos); if (min_time < 0 || t < min_time) min_time = t; } Event first_ev = new Event(); first_ev.type = 0; first_ev.time = min_time; event_list.Add(first_ev); // 各トラックのうち、現在の時間+デルタタイムが最も小さいものはどれか? // その命令を解析する while (pos[0] != -1 || (pos.Length >= 2 && pos[1] != -1)) { Event ev = _get_command(data, pos, before_command, time); event_list.Add(ev); } // メンバの変更 this.filename = filename; } } } } }
これを使って、以下のようなMIDIファイルを解析し、Unity上で表示した。Un-Demystifed Fantasy の主旋律である。
なんか左に詰めて表示されるなぁ…。
フォーマット0にして、もう一度やってみた。
今度は上手く行った。フォーマット0推奨にしたほうが良いかもしれない。まぁ、必要なのは主旋律データだけだし、フォーマット0でも問題ないか…。
バイナリデータ弄ったり、信号変換したりの作業は楽しかったが、実に非人間的であった。二度とやりたくない。
5.4 マイク入力の音程解析
注文したマイクが届いた。
ちなみに、コレである。ケーズデンキの通販で買って2400円位だった。安い。
正しい用途は動画投稿サイトへの歌の投稿らしい。
SONY エレクトレットコンデンサーマイクロホン PCV80U ECM-PCV80U
- 出版社/メーカー: ソニー(SONY)
- 発売日: 2011/10/10
- メディア: エレクトロニクス
- 購入: 32人 クリック: 127回
- この商品を含むブログ (8件) を見る
サウンドライブラリ
マイク入力については少し置いておき、先日までに書いた音程解析メソッドを整理してライブラリとする。
using UnityEngine; using System.Collections; public static class SoundLibrary{ // オーディオの周波数を調べる // ac: 解析したいオーディオソース // qSamples: 解析結果のサイズ // threshold: ピッチの閾値 public static float AnalyzeSound(AudioSource ac, int qSamples, float threshold) { float[] spectrum = new float[qSamples]; ac.GetSpectrumData(spectrum, 0, FFTWindow.BlackmanHarris); float maxV = 0; int maxN = 0; //最大値(ピッチ)を見つける。ただし、閾値は超えている必要がある for (int i = 0; i < qSamples; i++) { if (spectrum[i] > maxV && spectrum[i] > threshold) { maxV = spectrum[i]; maxN = i; } } float freqN = maxN; if (maxN > 0 && maxN < qSamples - 1) { //隣のスペクトルも考慮する float dL = spectrum[maxN - 1] / spectrum[maxN]; float dR = spectrum[maxN + 1] / spectrum[maxN]; freqN += 0.5f * (dR * dR - dL * dL); } float pitchValue = freqN * (AudioSettings.outputSampleRate / 2) / qSamples; return pitchValue; } // ヘルツから音階への変換 public static float ConvertHertzToScale(float hertz) { float value = 0.0f; if (hertz == 0.0f) return value; else { value = 12.0f * Mathf.Log(hertz / 110.0f) / Mathf.Log(2.0f); // while (value <= 12.0f) value += 12.0f; // while (value > 36.0f) value -= 12.0f; return value; } } // 数値音階から文字音階への変換 public static string ConvertScaleToString(float scale) { // 12音階の何倍の精度で音階を見るか int precision = 2; // 今の場合だと、mod24が0ならA、1ならAとA#の間、2ならA#… int s = (int)scale; if (scale - s >= 0.5) s += 1; // 四捨五入 s *= precision; int smod = s % (12 * precision); // 音階 int soct = s / (12 * precision); // オクターブ string value; // 返す値 if (smod == 0) value = "A"; else if (smod == 1) value = "A+"; else if (smod == 2) value = "A#"; else if (smod == 3) value = "A#+"; else if (smod == 4) value = "B"; else if (smod == 5) value = "B+"; else if (smod == 6) value = "C"; else if (smod == 7) value = "C+"; else if (smod == 8) value = "C#"; else if (smod == 9) value = "C#+"; else if (smod == 10) value = "D"; else if (smod == 11) value = "D+"; else if (smod == 12) value = "D#"; else if (smod == 13) value = "D#+"; else if (smod == 14) value = "E"; else if (smod == 15) value = "E+"; else if (smod == 16) value = "F"; else if (smod == 17) value = "F+"; else if (smod == 18) value = "F#"; else if (smod == 19) value = "F#+"; else if (smod == 20) value = "G"; else if (smod == 21) value = "G+"; else if (smod == 22) value = "G#"; else value = "G#+"; value += soct + 1; return value; } // 数値音階から生波形を出す public static void ScaleWave(float[] scale, int size, LineRenderer line) { line.SetVertexCount(size); float x = -150.0f; for (int i = 0; i < size; i++) { line.SetPosition(i, new Vector3(x, -80.0f + scale[i] * 2.5f, 0)); x += 1.0f; } } }
マイク入力の音程解析
簡単に言うと、以下の様な仕組みである。
- AudioSourceのclip(流すファイル)とマイク入力を関連付ける
- そのオーディオを再生させる(録音音声を聞きたくない場合はミュートにする)
- 前回の記事の要領で音程解析を行う
重要なのは以下のメソッド。
AudioClip Microphone.Start(string deviceName, bool loop, int lengthSec, int frecency)
deviceName
録音に使うマイクの名前。
loop
録音時間が尽きたら最初から録音を再開するか
lengthSec
録音時間
frecency
サンプリング周波数
以下が音程解析ソース。以前までの記事に詳しいことは書いているので、特に説明は必要ないと思う。
using UnityEngine; using System.Collections; public class MicroFFT : MonoBehaviour { // 波形を描画する public LineRenderer line; // マイクからの音を拾う public AudioSource mic; private string mic_name = "UAB-80"; // 波形描画のための変数 private float[] wave; private int wave_num; private int wave_count; void Start () { // 波形描画のための変数の初期化 wave_num = 300; wave = new float[wave_num]; wave_count = 0; // micにマイクを割り当てる mic.clip = Microphone.Start(mic_name, true, 999, 44100); if (mic.clip == null) { Debug.LogError("Microphone.Start"); } mic.loop = true; mic.mute = true; // 録音の準備が出来るまで待つ while (!(Microphone.GetPosition(mic_name) > 0)) { } mic.Play(); } void Update () { // 諸々の解析 float hertz = SoundLibrary.AnalyzeSound(mic, 1024, 0.04f); float scale = SoundLibrary.ConvertHertzToScale(hertz); string s = SoundLibrary.ConvertScaleToString(scale); Debug.Log(hertz + "Hz, Scale:" + scale + ", " + s); // 波形描画 wave[wave_count] = scale; SoundLibrary.ScaleWave(wave, wave_count, line); wave_count++; if (wave_count >= wave_num) wave_count = 0; } }
これで、適当にマイクに向かって発声してみました。
なかなか綺麗に出ます。さすが単一指向マイクです。
ちなみに、私の声域(マイクが拾ってくれた音)は約98Hz~630Hzでした。およそ2オクターブ半なので、まぁ妥当なところだと思います。
AudioSourceをもう一つ追加し、BGMを流しながらちょっとだけ歌いました。
CROSSOっぽくなってきました。
課題
- 最初のテストでは2秒の遅延が生じた(以降はほぼ遅延なし)。どうやら、録音準備が出来て実際に録音がスタートしてから、mic.Play()が有効化するのに時間がかかると遅延が発生するらしい。これは回避手段があるのだろうか…。あるとすれば、録音中の時間と再生時間を合わせるくらいか(出来るのか)。
- 「あ」から「ん」まで発音したが、さ行とた行辺りでノイズが強く発生した。また、「る」でも大きく波がブレた。もしかして、「る」の特殊歌唱ってあまり良くなかったのでは…
5.3 FFTとオーディオの音程解析
マイクがまだ届いていないので、とりあえずオーディオファイルの音程を解析する部分を作った。
音程解析とは
オーディオファイルの音程を解析するには、以下の様な手順を踏む。
ノイズの影響を受けやすく、めっちゃ難しい。
FFT(高速フーリエ変換)
FFT(Fast Fourier Transform)とは、離散フーリエ変換の高速アルゴリズムである。具体的にはO(n^2)がO(nlogn)になる。これがどれほど凄いことかというと、たとえばn=1000のときを考えて見れば良い。前者は100万になり、後者は約1万になる*1。計算時間が100分の1になっている。リアルタイムに離散フーリエ変換を行う際、FFTはほぼ必須となってくる。
フーリエ変換とは、まぁ簡単に言うとx軸が時間・y軸が音圧だったものをx軸が周波数・y軸が分布に変えることである。つまり、オーディオファイルのある時間帯にどのくらいの周波数の音がどのくらい存在しているかが分かる。
詳しくはWikipediaに投げる。
Unityのココが凄い
Unityには、初めからオーディオファイルの高速フーリエ変換を行う関数が存在しています。
audio.GetSpectrumData(float[] samples, int channel, FFTWindow window);
これで、チャンネルchannelの*2オーディオファイルのFFT結果をsamplesに入れることが出来ます。samplesのサイズは2の乗数である必要があります。まぁ、1024を使うのが無難っぽい?windowは窓関数というものらしく、なんだかよく分かりませんがノイズを軽減する効果があるみたいです。だいたいFFTWindow.BlackmanHarrisを入れておけばいいと思います。
これで得られるデータは、Nをsamplesのインデックスナンバー・FをAudioSettings.outputSampleRateの値・Qをsamplesのサイズとすると、で与えられる周波数(Hz)です。
ピッチを探すには、samplesの中で最も大きな値を持っているものを探せば良いです。これがおおよそピッチの周波数になるのですが、配列の各要素は飛び飛びの周波数の分布を表しているので、周辺の分布にもピッチが影響している可能性があります。
そこで、なんだかよく分かりませんが魔法の数式を使います。ピッチとして検出したインデックスナンバーをNとし、samples[N](と略記します)にを足します。この式、本気で意味がわからないですが、確かに誤差が抑えられる効果を持っています。
ちなみに参考にしたのはここです。これのほぼ丸パクリになりますが、音程を解析するAnalyzeSound関数を定義します。
private int qSamples = 1024; //配列のサイズ private float threshold = 0.04f; //ピッチとして検出する最小の分布 private float pitchValue; //ピッチの周波数 private float[] spectrum; //FFTされたデータ private float fSample; //サンプリング周波数 void AnalyzeSound() { audio.GetSpectrumData(spectrum, 0, FFTWindow.BlackmanHarris); float maxV = 0; int maxN = 0; //最大値(ピッチ)を見つける。ただし、閾値は超えている必要がある for (int i = 0; i < qSamples; i++) { if (spectrum[i] > maxV && spectrum[i] > threshold) { maxV = spectrum[i]; maxN = i; } } float freqN = maxN; if (maxN > 0 && maxN < qSamples - 1) { //隣のスペクトルも考慮する float dL = spectrum[maxN - 1] / spectrum[maxN]; float dR = spectrum[maxN + 1] / spectrum[maxN]; freqN += 0.5f * (dR * dR - dL * dL); } pitchValue = freqN * (fSample / 2) / qSamples; }
これを使って、440Hzの正弦波の分布を見ると以下のようになります。
多少誤差はありますが、まぁまぁ正確な周波数が出ています。
周波数から数値音階への変換
さて、ここからはカラオケ採点として数値を楽に扱うために、周波数から数値音階への変換方法を考えます。
数値音階というのは私の造語です。ここでは以下のように定義します。
数値音階とは、以下の性質を満たす実数である。
- 110Hzを0とする
- ある数値音階から1大きい数値音階は、半音上の音階を表す
- ある数値音階から1小さい数値音階は、半音下の音階を表す
これは何をするためのものかというと、CROSSOのような歌唱線を作るためのものです。
ところで、音階と周波数に関する基礎知識を整理しましょう。
- 440HzはA(ラ)の音
- 周波数が2倍になるとちょうど1オクターブ高い音となる
以降、ドレミファソラシドをCDEFGABと表します。上の基礎知識は、つまり110Hz・220Hz・440Hz・880Hz…はAの音ということです。これをそれぞれA1・A2・A3・A4…と定義します。
A1からA2の間には、嬰音(#の付いた音)も考えると12音存在します。この12音は、110Hzから220Hzの間に均等に配置されています。
つまり、x=0をA1・x=1をA#1・x=2をB1…として、yを対応する周波数とすると、という関係式が出来ます。これを整理すると、となります。ここで、logは自然対数です。
また、数値音階を素に最も近い音階を文字列で表示できるようにすると便利でしょう。今回は四半音まで判断できるようにしました(たとえば、A1とA#1の間の音はA1+と表します)。
以下がコードになります。
// ヘルツから音階への変換 float ConvertHertzToScale(float hertz) { if (hertz == 0) return 0.0f; else return (12.0f * Mathf.Log(hertz / 110.0f) / Mathf.Log(2.0f)); } // 数値音階から文字音階への変換 string ConvertScaleToString(float scale) { // 12音階の何倍の精度で音階を見るか int precision = 2; // 今の場合だと、mod24が0ならA、1ならAとA#の間、2ならA#… int s = (int)scale; if (scale - s >= 0.5) s += 1; // 四捨五入 s *= precision; int smod = s % (12 * precision); // 音階 int soct = s / (12 * precision); // オクターブ string value; // 返す値 if (smod == 0) value = "A"; else if (smod == 1) value = "A+"; else if (smod == 2) value = "A#"; else if (smod == 3) value = "A#+"; else if (smod == 4) value = "B"; else if (smod == 5) value = "B+"; else if (smod == 6) value = "C"; else if (smod == 7) value = "C+"; else if (smod == 8) value = "C#"; else if (smod == 9) value = "C#+"; else if (smod == 10) value = "D"; else if (smod == 11) value = "D+"; else if (smod == 12) value = "D#"; else if (smod == 13) value = "D#+"; else if (smod == 14) value = "E"; else if (smod == 15) value = "E+"; else if (smod == 16) value = "F"; else if (smod == 17) value = "F+"; else if (smod == 18) value = "F#"; else if (smod == 19) value = "F#+"; else if (smod == 20) value = "G"; else if (smod == 21) value = "G+"; else if (smod == 22) value = "G#"; else value = "G#+"; value += soct + 1; return value; }
さらに、CROSSOみたいに波形が出るような機構を作りました。これについてのコードは省略します。
これを用いて、C2からC3までの正弦波を流しました。
いい感じだったので、試しに普通の楽曲を流してみました。これは初音ミクの激唱です。
う~ん、なかなか絶望的な感じ。なお、閾値以上の音が取れていない場合は最下点に持って行っています。
とにかくBGM(特にドラム)が妨害してくる感じなので、ボーカルのみの曲を探しました。
これなんか、解析できないと話にならなさそうですね。やってみました。
まぁまぁという感じでしょうか。ここまで綺麗なデータでもノイズの影響は出るみたいです。音程はそこそこちゃんと取れていますが、何故かオクターブ違いで取ってしまっている箇所がありますね。JOYSOUNDのアレって普通に起こり得る現象なんですね。
課題
- ノイズによって波形が乱れる現象について考える
5.2 Unityでオーディオファイルを利用する
新しい設計
というわけで、カラオケ採点機のための新しい設計を考えました。
- オーディオファイル(wavとか)を鳴らす
- 主旋律やテンポなどのデータを取ったファイルをMIDIとして用意、読み込んでwavファイルと同期させる
この設計にすることで、以下の様な利点と欠点が生まれます。
利点
- 原曲音源でもカラオケが出来る
- MIDIの解析が楽になる
- 既存のUnity機能を使って楽に制作できる
Unityでオーディオファイルを扱う
MIDIの解析が山場になることが確定しましたが、とりあえずUnityでオーディオファイルを扱う方法についてメモ。
空のGameObjectを作成し、Add Component -> Audio -> Audio Source。上のようなものがInspectorに出てきます。
音源を対象プロジェクトのAssetsフォルダに追加します。Unityに戻ると自動で同期されます。オーディオファイルの場合、同期に時間がかかる場合もあるみたいです。
こんな感じにインポート出来るはずです(画像のオーディオファイルは作業時に聞いていたものです)。
先ほど作成したAudio SourceのAudio Clipにドラッグしておきます。
さて、楽しいコーディングです。
Add ComponentからC#スクリプトを加えます。名前はAudioにでもしておきましょう。
以下の様なコードを書きましょう。
using UnityEngine; using System.Collections; public class Audio : MonoBehaviour { public int start_time; // Use this for initialization void Start () { audio.Play(); audio.time = start_time; } // Update is called once per frame void Update () { Debug.Log(audio.time); } }
audio.Play()
このGameObjectに割り当てられたAudio Sourceを再生する。
audio.time
このGameObjectに割り当てられたAudio Sourceの再生位置を設定および読み込みする。
フレームごとに再生時間を出力します。
start_timeを弄ってから再生することで、開始時間を変更できます。
音質がものすごく劣化するけど、回避方法はあるのかな。
参考文献
OSC, MIDI プラグイン (Unity) – Keijiro Takahashi
オーディオファイルの再生と動機して、MIDIデータから譜面を読み込み何だかんだするライブラリ(SmfLite)があります。ここから新設計の着想を得ました。
keijiro_smflite at test · GitHub.htm
SmfLiteのサンプルです。ここからUnityのAudioについて知見を得ました。
5.1 Unityで波形を表示する
というわけで、もう一つのブログに書いてあった通り、カラオケ採点機を作っていく。
春合宿での発表のためにも、こまめにノート(記事)を取っていくことにした。
記事内で扱う内容は赤文字、既に終わった内容は灰文字で表示する*1。
Unity
たった一日しか触っていないので、かなり適当言ってる可能性があることをご了承ください。
Unityとは、最近流行っているゲーム製作エンジンである。公式サイトはここ。
このエンジンの注目すべき特徴は以下の通り。
- 3Dゲーム製作に強い
- マルチプラットフォームである
この中で、特にマルチプラットフォームであることが注目を浴びている。つまり、Unity上であるゲームを完成させれば、それをWindows用にもMac用にもLinux用にもAndroid用にもiOS用にもビルドできるということである。家庭用ゲーム用にもビルド出来るらしい。凄いですね。
使用するために、公式サイトから適当にファイルを落としてインストールしましょう。なんか登録が必要だった気がするのでメールアドレスを用意しましょう。有料版もありますが、無料版で十分だと思うので無料版をインストール。
以下、記事内容はWindows7上のUnity4.3.3f1を想定しています。
Unityでのゲーム製作
インストールできたら起動しましょう。
普通は3Dに飛びつきますが、今回は2Dで事足りるので2Dを選択してNewProjectをCreateしましょう。
Projectフォルダを設定し、左下のやつを2Dに変えて、Createを押します。チェックボックスは特に何もつけなくて良いです。
こんな感じの画面が出ると思います。なんか余計なウィンドウが出るかもしれないですが、消してください。
順番に説明しましょう。
Scene
左上のSceneっていう枠のこと。
Unityでは、Project(=ゲーム)はSceneの集まりとして扱う。つまり、爆幻をProjectとして例にすると、Sceneはタイトル画面・実際のゲーム画面、という感じになる。Game
Sceneの下にあるGameという枠のこと。
実際のゲーム画面を表す。Hierarchy
真ん中あたりのHierarchyという枠のこと。
現在のSceneに存在するGameObjectの一覧がある。GameObjectはUnityでのゲーム構成要素の末端要素のようなもの。爆幻でいうと、プレイヤーキャラであったりタイトルロゴであったりする。Projects
Hierarchyの横にあるProjectsという枠のこと。
Favorites
私も何か知らない┐(└┌)┘
Assets
全Scene共通で使えるパーツのこと。画像素材であったり音楽素材であったり共通ソースであったりを突っ込むらしい。
Inspector
一番右にあるInspectorという枠のこと。
GameObjectをクリックすると、ここにその詳細(Components)が出てくる。
初めはMain CameraというGameObjectが存在します。クリックしてみると、以下の様な画面が出ると思います。
この画面で、InspectorにはMain CameraのComponentsが表示されています。Transform・Camera・GUILayer・Flare Layer・Audio Listenerですね。それぞれに細かい要素があります。たとえば、Transformを見ると、Position・Rotation・Scaleですね。それぞれ、メインカメラの座標・回転・拡縮を表します。これらは、この画面で数値を変更することも可能ですし、C#/JavaScript/Booで書いたスクリプトによって制御することも可能です。今回はC#を使います。
まとめると、こんな感じです。
- Project(ゲームそのもの)
- Scene(ゲームの一場面)
- GameObject(シーンの構成要素。キャラとかロゴとか)
- Component(オブジェクトの構成要素。座標とか)
- GameObject(シーンの構成要素。キャラとかロゴとか)
- Scene(ゲームの一場面)
Hello World!
上のメニューからGameObject -> Create Empty。
こんな感じで、空のGameObjectが出来ます。F2か右クリック -> RenameかInspectorから「Hello」にリネームしておきましょう。
Inspectorから「Add Component」を選びます。New Script -> LanguageをC SharpにしてCreate and add。
ここから、別エディタを使ってC#のコードを書いていきます。なんかよく分からんエディタがデフォルトになってるので、適当に使い慣れたエディタに設定変更しましょう。
上のメニューから、Edit -> Preferences -> External Tools -> External Script Editorを好きなものに変更しましょう。私はMicrosoft Visual Studioにしました。
エディタの設定が終わったら、AssetsにあるC#のコードをダブルクリック(または、クリックからInspectorのopen)。VS立ち上がりOSEEEEEE!!!!
…とりあえず、こんな感じのコードが出るはずです。
using UnityEngine; using System.Collections; public class NewBehaviourScript : MonoBehaviour { // Use this for initialization void Start () { } // Update is called once per frame void Update () { } }
詳しいことは置いといて、メソッドの説明。と言っても、コード内にちゃんと英語で書いてあるのですが…。
void Start()
ゲームの起動時に一度だけ実行される内容。initみたいなもんですね。
void Update()
フレームごとに実行される内容。
というわけで、HelloWorldを書きます。
void Start () { Debug.Log("Hello World!"); }
書いたら保存して、Unityに戻ります。
Inspectorの中身も更新されればOKです。上のメニューから、Window -> Consoleでコンソールウィンドウを出します。
Clearでコンソールウィンドウの中身をクリアできます。しておきましょう。
Hierarchyの上の方にある再生/一時停止/1フレームごとに再生を表すボタンのうち、再生を押してみましょう。実行にはやや時間がかかります。
こんな感じになればOK。
正弦波を描画する
まず、カメラを引いておきます。
HierarchyのMainCameraをクリックし、Sceneの上辺りにある十字のマーク(左から2番め)をクリック。Sceneの上でマウスホイールを下に回すと、カメラが引いていきます。
初め、グリッドは10刻みで表示されていますが、引きまくると100刻みになります。とりあえずx座標が-200~200まで入るようにしましょう。その後、真ん中の白い長方形の辺上にある点をドラッグし、カメラ範囲をx座標が-200~200まで入るようにします。
GameObject -> Create Emptyで新しいGameObjectを作ります。名前はsinにでもしておきましょう。
コンポーネントとして、Effects -> LineRendererを追加します。
LineRendererは、指定した複数座標を直線で繋ぎ、描画します。今回だと正弦波の描画を担当します。
作ったコンポーネントのParametersを開き、Start ColorとEnd Colorに適当な色を設定します。
また、MaterialsのElement0をSprites Defaultにしておきましょう。横のラジオボタンみたいなのを押せばOK。
こんな感じにしましょう。
コンポーネントとして、再びC#スクリプトを追加します。スクリプト名はsinにでもしておきましょう。
以下のようにコードを書いてください。
using UnityEngine; using System.Collections; public class sin : MonoBehaviour { public LineRenderer lr; private int theta; private int count; // Use this for initialization void Start () { theta = 0; count = 0; lr.SetVertexCount(count); } // Update is called once per frame void Update () { if(count > 400) count = 0; count++; lr.SetVertexCount(count); lr.SetPosition(count - 1, new Vector3(-200 + count, 80 * Mathf.Sin(theta * Mathf.PI / 180), 0)); theta++; } }
説明が必要そうなものだけ、順番に説明しましょう。
public LineRenderer lr;
publicにするとソース中で操るコンポーネントを指定することが出来ます。
このコードを保存した後、InspectorのSinを見てください。
こんな感じで、lrにコンポーネントを指定することが出来ます。
この上に、先ほど作ったLineRendererのコンポーネントをドラッグしてみましょう。すると、それが割り当てられるはずです。
privateだと、この項目は出てきません。private int theta;
描く正弦波のラジアンを表します。
private int count;
実行からの時間を表します。ただし、400フレーム周期です。
lr.SetVertexCount(int n);
LineRendererの頂点数を指定します。
lr.SetPosition(int index, Vector3 pos)
LineRendererの第index頂点の座標を設定します。
さて、再生してみましょう。以下の様な正弦波がループしていれば成功です。
4.1 コンちゃん(Lunatic)について
おちんちんっていうツイートする。その各々でいいとする必要単位を開発してる
— コンちゃん(Lunatic) (@lunatic_club) 2013, 12月 5
前のコンちゃんはEasyBotterを利用していたためPHPで作成していたが、色々と自由度が減る上にcronサービス*1が不安定だったため、あまり良い出来とは言えなかった(ウザさは一級品だったが)。
というわけで、今回はRubyとTwitter gemを使ってTwitter botを作成した。
その軌跡について、かなり詳しく書いてみる。
準備
実は、今回のbot作成はRubyとgemの扱いに慣れることが主目的だったりする(他にやりたいことがある)。
開発環境は様々なことを考慮してLinuxの方が良いので、Virtual Boxを利用してUbuntu12.04をWindows7に入れた。実は既にUbuntuはデュアルブートさせているのだが、いちいち再起動するのがめんどいのと作業中に遊びが欲しいのでWindowsと同時に使えるVirtual Boxを選択した。
Ubuntuの何がいいかって、欲しいパッケージは魔法の呪文「sudo apt-get install hogehoge」でダウンロードからインストールまで全部やってくれるってところ。Androidと同じような感じ。開発環境とかWindowsより圧倒的に楽に準備できる。あと、Terminalカチャカチャやってるとなんだかカッコいいです。
Ruby
プログラミング言語Rubyは、日本人によって作られたオブジェクト指向言語である。
と言っても、Rubyは恐ろしく自由度が高い言語で、そもそもmain関数というものが存在しない。基本的にはコードの一番上から順番に実行される。でも、関数やクラスを用いてパッケージングも出来る。
puts 'Hello World!!' # RubyのHello World
数字や演算子に至るまですべてがオブジェクト(Java風にいうとインスタンス)であり、それらには初めから基本メソッドが存在している。基本メソッドは結構な数があり、痒いところに手が届くように作られていて非常に快適にコーディング出来る。
108.to_s # 108を文字列に変換する string.to_i # 文字列stringを整数に変換する
また、変数宣言が必要ない。変数は、必要に応じて型が自動的に割り当てられる。型ももちろんオブジェクト。
hoge = 7 # 整数型になる st = '文字列' # 文字列型になる hoge = 'ふが' # 文字列型に変換される puts hoge # 'ふが'が表示される(7は表示されない)
配列は動的で、異なる型(クラス)のオブジェクトを1つに配列に入れることも可能。
list = [1, '2', hoge] # できる。
正規表現を気軽に扱えるのも強みだ。
st = 'this is a pen.' st = st.gsub(/[^ ]*is/, 'hoge') # //で囲まれた部分は正規表現と見なされる puts st # hoge hoge a pen.
また、対話環境も自動でついてくる。irbで起動できる。
ちなみに、インタプリタなので、
% ruby hogehoge.rb
これでプログラムを実行する。今回はRuby1.9.3で作成した。
gem
Rubyに必要なライブラリを自動的に落としてくれる便利なもの。
% gem install hogehoge
これでhogehogeというライブラリを落としてきてくれる。重いものを落としているとフリーズしたように見えるのが欠点。
今回はgem1.8.23を使用した。
必要なライブラリを落としまくる
先ほどのgemでどんどん入れていく。以下、gem install hogehogeを使うこと。
Twitter APIを叩く便利なもの。たぶん公式ドキュメント→http://rdoc.info/gems/twitter
自由度がとても高い。すごい。
アカウントを準備する
アカウントを作って、Twitter Developersでログイン。新しくアプリケーションを作成する。OAuthのAccess LevelはRead and Writeとしておき、アプリの管理画面からAccess Tokenを生成する。
Access Tokenの生成が終わったら、Consumer Key、Consumer Secret、Access Token、Access Token Secretの値をメモっておく。
コードを書く
とりあえず呟かせてみよう。
# -*- coding: utf-8 -*- require 'twitter' # アクセス設定 client = Twitter::REST::Client.new do |config| config.consumer_key = "さっき取得したやつ" config.consumer_secret = "さっき取得したやつ" config.access_token = "さっき取得したやつ" config.access_token_secret = "さっき取得したやつ" end client.update("テスト")
これで「テスト」と投稿されるはずである。
その他の例については、公式(たぶん)を見て欲しい。
マルコフ連鎖
おちんちんっていうツイートする。その各々でいいとする必要単位を開発してる
— コンちゃん(Lunatic) (@lunatic_club) 2013, 12月 5
コンちゃんは、このような文章を延々と吐いている。これは、コンちゃんのTLから拾った文章を形態素解析して、マルコフ連鎖でランダムに繋げて文章を生成しているのである。
マルコフ連鎖の数学的定義は置いといて、コンちゃんでは以下の様な仕組みで文章を生成している。
- 文章を形態素にバラす。この時、先頭には'__BEGIN__'タグを、最後には'__END__'タグをつける。
- それぞれ2つずつをペアにして配列に入れる。たとえば、"私はLです"だと["__BEGIN__","私],["私","は"],["は","L"],["L","です"],["です","__END__"]のようになる。
- 溜まった形態素ペア群からまず__BEGIN__を探してきて、ランダムに選択。次に、__BEGIN__とペアになっている単語を生成文章の後ろに追加し、次はペアになっていた単語から始まる形態素ペア群をランダムに選択、それを__END__が現れるまで繰り替えす。
形態素ペアはグループにする数を増やせば増やすほどまともな文章が現れる確率は上がる(もっとも、下手に数を増やしても元の文章とそのままのものが出てくる確率が上がってあまりおもしろくない)。今回だとグループは2つの要素を持つので、2次マルコフ連鎖と呼ばれるらしい??
かの有名なしゅうまい君も、基本的にはこのような仕組みで動いている。
というわけで、マルコフ連鎖部分だけコードを載せる~~
# -*- coding: utf-8 -*- def make_markov(texts) # textsから2次マルコフ連鎖で文章を生成。中身は1次配列。 message = '' # 返す文章 next_front = '__BEGIN__' # 次のデータの先頭文字 while 1 > 0 list = [] #候補に上がったデータ (texts.length).times do |m| if m%2 == 0 # データの先頭文字とnext_frontが一致すれば、その後の文字をリストにpush if texts[m] == next_front list = list + [texts[m+1]] end end end if list.length == 0 # リストが空なら終了。 message = '__FAILED__' break elsif message.length > 140 # 140文字以上なら終了。 message = '__FAILED__' break else m = list[rand(list.length)] if m == '__END__' break else message = message + m next_front = m end end end return message end
cronの設定
botのコードが無事書けたら、最後はcronを設定して定期的に実行させてやろう。
Ubuntuの場合、以下のように叩けば良い。
% crontab -e
初めて起動するときは使用エディタを聞いてくる。私の場合はemacsなのでemacsを選んだ。
なんかいっぱいコメントが書いてあるが、最後の行に以下のように書こう*2。
0,30 * * * * cd /hoge/fuga; /hoge/.rbenv/shims/ruby /hoge/fuga/bot.rb
これで、毎時0分と30分に"% ruby bot.rb"を起動してくれる。cdでrbファイルがある場所に移動しとく必要があるのと、すべてフルパスで書く必要があることに注意。
もちろん、Ubuntuの起動中にしか動かないぞ☆
あと、
% crontab -r
これは、設定したcronを確認なしで全て消去するコマンドなので注意。eとrは隣にあるからタイプミスし易いのだが…。色々と対策はあるので気になる人は各自対策しよう。