curious4dev

中国旅行、Arduinoなどを使った電子工作、その他色々。

*

Androidで8bitサウンドをモノフォニックな感じで実装してみた。

 

お疲れ様です。高橋です。

Arduino+YMZ294で、8bitサウンドがマイブームなわけですが、Androidで出来ないかどうか探り中です。

いつものように「Android 8bit 音」でググると、いくつか出てきました。その中で秀逸だったのが、こちらのサイト。クラスメソッドという会社の小室 啓さんという方が書いた、

【クリスマスだし】Androidで8ビット音を生成してジングルベルを奏でてみる

Android上でAudioTrackというクラスを使い、周波数レベルで音の生成が出来るようです。

上記参考サイトはタイトルの通り、内部で周波数とミリ秒を指定してモノフォニックなジングルベルを奏でているのですが、私は最終的に2本の指でベース部とメロディ部、デバイスの傾きで音量を調節出来るような物を作れるかどうか実験したいと思います。

おそらく必要な技術は

  1. マルチタッチの認識
  2. タッチ時間中、音をずっと鳴らしておく
  3. タッチの位置と音階を比例させる
  4. 右の指(メロディ部)と左の指(ベース部)をいい感じに分ける
  5. 傾きセンサーで音量を上げ下げする
  6. 和音を出す

あたりかと思います。全てがヘビー級の難易度です。。

まずは、いつものように上記参考サイトのコピペ&修正からです。最初に潔くジングルベルを撤去し、ボタンレベルで音階を表現出来るかどうかの実験を行いました。今回は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を押すと音が鳴ります。まだ音の長さ等は全然実装していませんが、最終的にはマルチタッチを目指しているため、適当に作っています。

 

 

and01

 

 

以上、よろしくお願い致します。

 - アプリ開発

  関連記事

遅刻の言い訳提案システム 稼働初日

お疲れ様です。高橋です。 先日まで微調整を重ねてきた「遅刻の言い訳提案システム」 …

8bitサウンドを出すAndroidアプリをリリースしてみた。

お疲れ様です。高橋です。 先日の「Androidで8bitサウンドをモノフォニッ …

8bit音アプリを実機デバッグし、モスキート音モードを追加してみた。

お疲れ様です。高橋です。 今日会社でAndroidを持っている人にインストールし …

リリースアプリ群が累計400ダウンロード突破

お疲れ様です。高橋です。 スマートフォン向けアプリ群が2015年5月13日(水) …

選挙運動シミュレータ「衆院選2014」が迷走してきた。

お疲れ様です。高橋です。 選挙運動シミュレーションゲーム「衆院選2014」の見た …

街頭インタビュー リリースしてみた

お疲れ様です。高橋です。 リリース 街頭インタビューアプリをリリースしました。 …

街頭インタビュー 実装 #2

お疲れ様です。高橋です。 本日の実装状況 街頭インタビューを構成する要素を、徹底 …

androidアプリから総務省APIをコールしてみる

お疲れ様です。高橋です。 androidアプリから総務省APIをコールする事に成 …

「カナかな?」の2週間分のダウンロード数

お疲れ様です。高橋です。 本日の貴重な帰宅後の時間は、妻からの「なんとかっていう …

アプリDL状況と言い訳システムの効果について

お疲れ様です。高橋です。 リリースしたアプリのDL状況 3/20(Fri)時点で …