Androidで8bitサウンドをモノフォニックな感じで実装してみた。
お疲れ様です。高橋です。
Arduino+YMZ294で、8bitサウンドがマイブームなわけですが、Androidで出来ないかどうか探り中です。
いつものように「Android 8bit 音」でググると、いくつか出てきました。その中で秀逸だったのが、こちらのサイト。クラスメソッドという会社の小室 啓さんという方が書いた、
【クリスマスだし】Androidで8ビット音を生成してジングルベルを奏でてみる
Android上でAudioTrackというクラスを使い、周波数レベルで音の生成が出来るようです。
上記参考サイトはタイトルの通り、内部で周波数とミリ秒を指定してモノフォニックなジングルベルを奏でているのですが、私は最終的に2本の指でベース部とメロディ部、デバイスの傾きで音量を調節出来るような物を作れるかどうか実験したいと思います。
おそらく必要な技術は
- マルチタッチの認識
- タッチ時間中、音をずっと鳴らしておく
- タッチの位置と音階を比例させる
- 右の指(メロディ部)と左の指(ベース部)をいい感じに分ける
- 傾きセンサーで音量を上げ下げする
- 和音を出す
あたりかと思います。全てがヘビー級の難易度です。。
まずは、いつものように上記参考サイトのコピペ&修正からです。最初に潔くジングルベルを撤去し、ボタンレベルで音階を表現出来るかどうかの実験を行いました。今回はMainActivity.javaに加えて2つ、合計3つのクラスが必要です。
DigitalSoundGenerator.java
DigitalSoundGeneratorは周波数から音を生成している、今回の一連のプログラムのコア部分にあたります。ここでsin波にしたり矩形波にしたりする事も出来ます。おそらく。
参考サイトのコードは音程を狭く取っていましたが、8オクターブ対応とします。
package jp.curious4dev.sound8bit; import java.util.HashMap; import java.util.Map; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; /** * ピコピコ音を作成する * * @author komuro * @editor Takahashi * */ public class DigitalSoundGenerator { public static Map<String, Double> noteMap = new HashMap<String, Double>() { private static final long serialVersionUID = 1L; { put("B0", 31.0); put("C1", 33.0); put("CS1", 35.0); put("D1", 37.0); put("DS1", 39.0); put("E1", 41.0); put("F1", 44.0); put("FS1", 46.0); put("G1", 49.0); put("GS1", 52.0); put("A1", 55.0); put("AS1", 58.0); put("B1", 62.0); put("C2", 65.0); put("CS2", 69.0); put("D2", 73.0); put("DS2", 78.0); put("E2", 82.0); put("F2", 87.0); put("FS2", 93.0); put("G2", 98.0); put("GS2", 104.0); put("A2", 110.0); put("AS2", 117.0); put("B2", 123.0); put("C3", 131.0); put("CS3", 139.0); put("D3", 147.0); put("DS3", 156.0); put("E3", 165.0); put("F3", 175.0); put("FS3", 185.0); put("G3", 196.0); put("GS3", 208.0); put("A3", 220.0); put("AS3", 233.0); put("B3", 247.0); put("C4", 262.0); put("CS4", 277.0); put("D4", 294.0); put("DS4", 311.0); put("E4", 330.0); put("F4", 349.0); put("FS4", 370.0); put("G4", 392.0); put("GS4", 415.0); put("A4", 440.0); put("AS4", 466.0); put("B4", 494.0); put("C5", 523.0); put("CS5", 554.0); put("D5", 587.0); put("DS5", 622.0); put("E5", 659.0); put("F5", 698.0); put("FS5", 740.0); put("G5", 784.0); put("GS5", 831.0); put("A5", 880.0); put("AS5", 932.0); put("B5", 988.0); put("C6", 1047.0); put("CS6", 1109.0); put("D6", 1175.0); put("DS6", 1245.0); put("E6", 1319.0); put("F6", 1397.0); put("FS6", 1480.0); put("G6", 1568.0); put("GS6", 1661.0); put("A6", 1760.0); put("AS6", 1865.0); put("B6", 1976.0); put("C7", 2093.0); put("CS7", 2217.0); put("D7", 2349.0); put("DS7", 2489.0); put("E7", 2637.0); put("F7", 2794.0); put("FS7", 2960.0); put("G7", 3136.0); put("GS7", 3322.0); put("A7", 3520.0); put("AS7", 3729.0); put("B7", 3951.0); put("C8", 4186.0); put("CS8", 4435.0); put("D8", 4699.0); put("DS8", 4978.0); } }; private AudioTrack audioTrack; // サンプリング周波数 private int sampleRate; // バッファ・サイズ private int bufferSize; /** * コンストラクタ */ public DigitalSoundGenerator(int sampleRate, int bufferSize) { this.sampleRate = sampleRate; this.bufferSize = bufferSize; // AudioTrackを作成 this.audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, // 音楽ストリームを設定 sampleRate, // サンプルレート AudioFormat.CHANNEL_OUT_MONO, // モノラル AudioFormat.ENCODING_DEFAULT, // オーディオデータフォーマットPCM16とかPCM8とか bufferSize, // バッファ・サイズ AudioTrack.MODE_STREAM); // Streamモード。データを書きながら再生する } /** * サウンド生成 * * @param frequency * 鳴らしたい音の周波数 * @param soundLengh * 音の長さ * @return 音声データ */ public byte[] getSound(double frequency, double soundLength) { // byteバッファを作成 byte[] buffer = new byte[(int) Math.ceil(bufferSize * soundLength)]; for (int i = 0; i < buffer.length; i++) { double wave = i / (this.sampleRate / frequency) * (Math.PI * 2); wave = Math.sin(wave); buffer[i] = (byte) (wave > 0.0 ? Byte.MAX_VALUE : Byte.MIN_VALUE); } return buffer; } /** * いわゆる休符 * * @param frequency * @param soundLength * @return 無音データ */ public byte[] getEmptySound(double soundLength) { byte[] buff = new byte[(int) Math.ceil(bufferSize * soundLength)]; for (int i = 0; i < buff.length; i++) { buff[i] = (byte) 0; } return buff; } /** * * @return */ public AudioTrack getAudioTrack() { return this.audioTrack; } }
SoundDto.java
ここではDigitalSoundGeneratorで生成した一音一音を管理しているようです。
package jp.curious4dev.sound8bit; public class SoundDto { // 音声データ private byte[] sound; // 長さ private double length; /** * 引数付きコンストラクタ * * @param source * @param length */ public SoundDto(byte[] source, double length) { this.sound = source; this.length = length; } public byte[] getSound() { return sound; } public void setSound(byte[] sound) { this.sound = sound; } public double getLength() { return length; } public void setLength(double length) { this.length = length; } }
MainActivity.java
MainActivityは、UIからの要求に基づいた音をDigitalSoundGeneratorで生成し、SoundDtoで管理し、別スレで再生する役割を持っているようです。
(シャープは、すっ飛ばしています。面倒なので)
package jp.curious4dev.sound8bit; import java.util.ArrayList; import java.util.List; import android.app.Activity; import android.media.AudioTrack; import android.os.Bundle; import android.view.View; public class MainActivity extends Activity implements Runnable { public static final double EIGHTH_NOTE = 0.125; public static final double FORTH_NOTE = 0.25; public static final double HALF_NOTE = 0.5; public static final double WHOLE_NOTE = 1.0; public double playNote = 440.0; // Sound生成クラス private DigitalSoundGenerator soundGenerator; // Sound再生クラス private AudioTrack audioTrack; // 譜面データ private List soundList = new ArrayList(); /** * 譜面データを作成 */ private void initScoreData() { // 譜面データ作成 soundList.clear(); soundList.add(new SoundDto(generateSound(soundGenerator, playNote, WHOLE_NOTE), 0)); soundList.add(new SoundDto(generateEmptySound(soundGenerator, EIGHTH_NOTE), EIGHTH_NOTE)); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // SoundGeneratorクラスをサンプルレート44100で作成 soundGenerator = new DigitalSoundGenerator(44100, 44100); // 再生用AudioTrackは、同じサンプルレートで初期化したものを利用する audioTrack = soundGenerator.getAudioTrack(); findViewById(R.id.C4).setOnClickListener(new View.OnClickListener() { public void onClick(View v) { playNote = DigitalSoundGenerator.noteMap.get("C4"); Thread th = new Thread(MainActivity.this); initScoreData(); th.start(); } }); findViewById(R.id.D4).setOnClickListener(new View.OnClickListener() { public void onClick(View v) { playNote = DigitalSoundGenerator.noteMap.get("D4"); Thread th = new Thread(MainActivity.this); initScoreData(); th.start(); } }); findViewById(R.id.E4).setOnClickListener(new View.OnClickListener() { public void onClick(View v) { playNote = DigitalSoundGenerator.noteMap.get("E4"); Thread th = new Thread(MainActivity.this); initScoreData(); th.start(); } }); findViewById(R.id.F4).setOnClickListener(new View.OnClickListener() { public void onClick(View v) { playNote = DigitalSoundGenerator.noteMap.get("F4"); Thread th = new Thread(MainActivity.this); initScoreData(); th.start(); } }); findViewById(R.id.G4).setOnClickListener(new View.OnClickListener() { public void onClick(View v) { playNote = DigitalSoundGenerator.noteMap.get("G4"); Thread th = new Thread(MainActivity.this); initScoreData(); th.start(); } }); findViewById(R.id.A4).setOnClickListener(new View.OnClickListener() { public void onClick(View v) { playNote = DigitalSoundGenerator.noteMap.get("A4"); Thread th = new Thread(MainActivity.this); initScoreData(); th.start(); } }); findViewById(R.id.B4).setOnClickListener(new View.OnClickListener() { public void onClick(View v) { playNote = DigitalSoundGenerator.noteMap.get("B4"); Thread th = new Thread(MainActivity.this); initScoreData(); th.start(); } }); } @Override protected void onPause() { super.onPause(); } @Override protected void onDestroy() { super.onDestroy(); // 再生中だったら停止してリリース if (audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { audioTrack.stop(); audioTrack.release(); } } @Override protected void onResume() { super.onResume(); } /** * 8ビットのピコピコ音を生成する. * * @param gen * Generator * @param freq * 周波数(音階) * @param length * 音の長さ * @return 音データ */ public byte[] generateSound(DigitalSoundGenerator gen, double freq, double length) { return gen.getSound(freq, length); } /** * 無音データを作成する * * @param gen * Generator * @param length * 無音データの長さ * @return 無音データ */ public byte[] generateEmptySound(DigitalSoundGenerator gen, double length) { return gen.getEmptySound(length); } @Override public void run() { // 再生中なら止める if (audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { audioTrack.stop(); } else { // 再生開始 audioTrack.play(); // スコアデータを書き込む for (SoundDto dto : soundList) { audioTrack.write(dto.getSound(), 0, dto.getSound().length); } // 再生停止 audioTrack.stop(); } } }
activity_main.xml
画面レイアウトです。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="jp.curious4dev.sound8bit.MainActivity" > <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" > <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <Button android:id="@+id/C4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/C4" /> <Button android:id="@+id/C4S" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/C4S" /> <Button android:id="@+id/D4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/D4" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <Button android:id="@+id/D4S" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/D4S" /> <Button android:id="@+id/E4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/E4" /> <Button android:id="@+id/F4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/F4" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <Button android:id="@+id/F4S" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/F4S" /> <Button android:id="@+id/G4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/G4" /> <Button android:id="@+id/G4S" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/G4S" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <Button android:id="@+id/A4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/A4" /> <Button android:id="@+id/A4S" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/A4S" /> <Button android:id="@+id/B4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/B4" /> </LinearLayout> </LinearLayout> </RelativeLayout>
res/values/strings.xml
layoutに貼り付けるbuttonのtextです。
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">Sound8bit</string> <string name="action_settings">Settings</string> <string name="C4">C4</string> <string name="C4S">C4#</string> <string name="D4">D4</string> <string name="D4S">D4#</string> <string name="E4">E4</string> <string name="F4">F4</string> <string name="F4S">F4#</string> <string name="G4">G4</string> <string name="G4S">G4#</string> <string name="A4">A4</string> <string name="A4S">A4#</string> <string name="B4">B4</string> </resources>
これで、下記のようにボタンが配置され、C, D, E, F, G, A, Bを押すと音が鳴ります。まだ音の長さ等は全然実装していませんが、最終的にはマルチタッチを目指しているため、適当に作っています。
以上、よろしくお願い致します。
関連記事
-
-
TwitterアナリティクスからCSVがダウンロード出来ない
お疲れ様です。高橋です。 TwitterアナリティクスのCSV 遅刻の言い訳提案 …
-
-
Androidアプリ上でLINEみたいな吹き出しでTextを囲んで表示してみた。
お疲れ様です。高橋です。 AndroidでLINEみたいにテキストの周りを吹き出 …
-
-
遅刻の言い訳提案システム 稼働四日目
お疲れ様です。高橋です。 本日も遅刻の言い訳提案システムのデータの分析と改善のた …
-
-
「遅刻の言い訳」実機デバッグ結果
お疲れ様です。高橋です。 本日、Androidを持っている同僚にお願いして、実機 …
-
-
アプリDL状況と言い訳システムの効果について
お疲れ様です。高橋です。 リリースしたアプリのDL状況 3/20(Fri)時点で …
-
-
遅刻の言い訳提案システム 稼働初日
お疲れ様です。高橋です。 先日まで微調整を重ねてきた「遅刻の言い訳提案システム」 …
-
-
「カナかな?」の2週間分のダウンロード数
お疲れ様です。高橋です。 本日の貴重な帰宅後の時間は、妻からの「なんとかっていう …
-
-
DAWを使って、ゲームの裏側で鳴らす音楽を作ってみた。
お疲れ様です。高橋です。 既に時期を逸してしまった、「選挙運動シミュレータ 衆院 …
-
-
西野カナ風な歌詞自動生成「カナかな?」をリリースしてみた。
お疲れ様です。高橋です。 昨日はずっと助詞に関する実装をしていたのですが、どうし …
-
-
街頭インタビュー 背景人物切り替え機能の実装
お疲れ様です。高橋です。 本日は、背景人物を切り替える機能を実装しました。とりあ …