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

 

 

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

 - アプリ開発

  関連記事

西野カナ風な歌詞自動生成「カナかな?」をリリースしてみた。

お疲れ様です。高橋です。 昨日はずっと助詞に関する実装をしていたのですが、どうし …

街頭インタビュー 背景人物切り替え機能の実装

お疲れ様です。高橋です。 本日は、背景人物を切り替える機能を実装しました。とりあ …

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

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

Twitter上に広がる寝坊市場について

お疲れ様です。高橋です。 遅刻の言い訳アプリ 初週DL数 2/26頃にリリースし …

上司離着席状態検知アプリ

お疲れ様です。高橋です。 上司離着席状態検知システムですが、 必要性を記載 回路 …

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

お疲れ様です。高橋です。 遅刻の言い訳提案システムについて、先日課題として上げた …

【完全版】 Androidで広告ID(Advertising ID)を取得する方法

お疲れ様です。高橋です。 非常に長い時間掛けて他人に実機デバッグをやってもらう事 …

街頭インタビュー 入力UI実装

お疲れ様です。高橋です。 今日は文字入力UI及び、背景人物の切り替え機能を実装し …

遅刻の言い訳提案システム 第一週最終日

お疲れ様です。高橋です。 今週は稼働→分析→修正→稼働、という繰り返しを何度か実 …

Win7(64bit)+cocos2dx-3.2でAndroid開発環境を構築してみた

お疲れ様です。高橋です。 ゲームを作る際に、自分で1から細々とした部品を作らなく …