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を押すと音が鳴ります。まだ音の長さ等は全然実装していませんが、最終的にはマルチタッチを目指しているため、適当に作っています。
以上、よろしくお願い致します。
関連記事
-
-
Google Play Developer登録
お疲れ様です。高橋です。 本日は引き続きwordpress周りの設定を行うと同時 …
-
-
遅刻の言い訳提案システムとウコンの力
お疲れ様です。高橋です。 現在稼働を続けている遅刻の言い訳提案システムは、改めて …
-
-
上司離着席状態検知アプリ
お疲れ様です。高橋です。 上司離着席状態検知システムですが、 必要性を記載 回路 …
-
-
「カナかな?」を実機デバッグし、ログ送出機能を追加してリリースしてみた。
お疲れ様です。高橋です。 デバッグしてみた 先日リリースした、西野カナ風の歌詞を …
-
-
Androidアプリ上でLINEみたいな吹き出しでTextを囲んで表示してみた。
お疲れ様です。高橋です。 AndroidでLINEみたいにテキストの周りを吹き出 …
-
-
【完全版】 Androidで広告ID(Advertising ID)を取得する方法
お疲れ様です。高橋です。 非常に長い時間掛けて他人に実機デバッグをやってもらう事 …
-
-
DLリンク付き言い訳提案システムとDL数の関係について
お疲れ様です。高橋です。 DLリンク付きの言い訳提案システムを1週間稼働させ、そ …
-
-
歌詞生成アプリ「カナかな?」の動詞をチューニングしてみた。
お疲れ様です。高橋です。 昨日はサーバ周りに熱中してしまってクライアント側を全く …
-
-
androidアプリから総務省APIをコールしてみる
お疲れ様です。高橋です。 androidアプリから総務省APIをコールする事に成 …
-
-
Unityでノベルゲームを作る
お疲れ様です。高橋です。 突然ですが、ノベルゲームを作ってみたくなったので、試し …

