zkさんの「シンプルな音デモ」を解析してみた(Player10)

2008/09/6

こんばんは。ようやくカード会社の審査がおりましてクレジットカードを入手しました。それでiPhoneをおかげ様でアクティベートできまして、いくつかのappをダウンロードした今日この頃です。
話題は少しそれるのですが、iPhoneを見てて思うのはARに向いてそうだな。ということです。というかiPhoneに限らずARを普及させるのって携帯が一番じゃないかと思うのです。すでにARを携帯でできる技術はできているみたいです。

iPhoneでARToolKitとかあるらしい。(タロタローグ ブログさん)
WindowsMobile用NyARToolkitデモプログラムの使い方(A虎さん)

いろいろと考えたのですが、QRコードをうまくパターンファイルにできないかとか、QRコードをみるとWebページのサムネがARで見えるようになるとか。
データ作成は大変そうだけど面白いかも。と思いついたのは、例のキューブ型マーカー(サイコロみたいの)をつかった商品説明です。片手にキューブ、片手に携帯をもって、キューブを携帯でちょうど虫眼鏡のようにのぞきこむのです。すると、キューブ上に商品の3D画像が表示されます。3Dでみせるショッピングサイトはすでにあると思いますが、ここでの売りは見せ方です。マウスと固定された画面の関係から離れて、商品を鑑定するようにいろいろな角度にキューブを回転させ携帯を虫眼鏡のように離したり近づけたりする。文章で書くと大したことなさそうですけど、実際に体験できると楽しそう。

さて、妄想がひといきついたところで、今回もPlayer10の音関係です。
今までいろいろとPlayer10のことを書いてきたんですが、サンプルがうまく見られないよー!というあなたのために、アドビさんはちゃんとスタンドアロン版を用意してくれています。ブラウザに入れるのは嫌!という方も、これはブラウザにインストールするわけじゃないから大丈夫ですよ。
Win:http://opensource.adobe.com/svn/opensource/flex/sdk/branches/3.1.0/in/player/10/win/FlashPlayer.exe
Mac:http://opensource.adobe.com/svn/opensource/flex/sdk/branches/3.1.0/in/player/10/mac/Flash%20Player.app.zip

さて、Playerの環境が整ったところで、今回のサンプルです。
ブラウザに入れている方はこちら(要Player10)


このサンプルはzkさんのこのエントリをもとに波形とか周波数の情報を見られるように改良したものです。zkさん、ありがとうございます。
[FLASH]APMT4で発表しました

この中の「シンプルな音デモ」が今回のサンプル部分です。それから、このエントリにあるTenoranもぜひ見てください。これ、本当に楽しいんですよ。ローパスフィルタとかディレイとか実装しててすごいです。svnでソースを落としてきてるんで、ぜひ今度解析したいと思ってます。
ちなみに、パワーアップ前のバージョンで勝手に私が改造しちゃったやつがこのエントリで見られます。zkさん申し訳ないです。

さて、「シンプルな音デモ」なんですが、これが意外とシンプルじゃなかったです。(いい意味で。)
マウスカーソルの位置を変えることで、音の高さや音色が変わります。最初どうやってやってるのか不思議だったんですが、分析してみたらなんとなくわかってきました。

音には3つの要素があります。
1:音色
2:高さ
3:音量
今回はこれら全てをマウスの操作で変更しています。ブラウザとPlayerだけでこんなことができるなんてすごいですよね!

まず音色をきめる部分です。音色は波形でわかります。なので、波形表示機能をつけてみました。カーソルを左にもっていくとサイン波に、右にもっていくと矩形波になることがわかります。サイン波は非常にきれいな音でにごりがありません。反対に矩形波はファミコンの音みたいに感じるんじゃないでしょうか。

次に音の高さです。マウスを上にもっていくと音は低く、下にもっていくと高くなることがわかります。
音の高さは周波数できまります。画面右下の方にfrequencyという表示をつけました。これが周波数です。たとえばこの値が440のときは440Hzということになります。周波数があがれば、音の高さもあがります。このとき波形にも注目してください。マウスを下に下げると、波の間隔がせばまっていきます。周波数とは1秒間に何回、波を上下させるかということと同じことです。周波数があがったので、1秒間に上下する回数もあがり、より多くの波がつめこまれていることが波形からわかります。

最後に音量です。音量は波の振幅で決まります。実際に画面左半分にいけばいくほど、上下の長さが長くなっていって音量があがっていきます。ただし、今回は左右にマウスを振ると音色の違う波を生成しているので、わかりづらいかもしれません。サイン波よりも矩形波の方が同じ振幅の場合、音量が大きく感じられるからです。

さて、そんな3つの要素を決めるコードなんですが、ソースでは以下の部分が該当します。

sample = Math.sin(phase) * shapeFactor + (phase < Math.PI ? 1 : -1 ) * (1 - shapeFactor) * 0.5;

細かい説明はおいておいて、この式を2つに分解してみました。

//A
Math.sin(phase) * shapeFactor

//B
(phase < Math.PI ? 1 : -1 ) * (1 - shapeFactor) * 0.5;

ここでプルダウンメニューの説明をします。「式 A + B」というのはもとのソースです。「式A」は上のソースの上の行。「式B」は上のソースの下の行です。
「式A」にあわせてみると、この式がサイン波を表していることが、「式B」にあわせてみると、この式が矩形波であることが、それぞれわかります。「式A」のとき右にいけばいくほど振幅が小さくなります、「式B」のときは反対に振幅が大きくなります。
つまり「式A + B」というもとのソースコードは、サイン波と矩形波を割合を変えて合成した波の音を出しているということなんですね。いや、面白い!

いかがでしたでしょうか?今回はPlayer10の音生成機能を用いて音の3要素について書いてみました。なんでこんなことを書いているかというと、音のプログラムをする人を増やしたかったからです。画像については、デジカメで何万画素とかいうぐらい世間的には広まっていて、実際画像に関してはFlashも含めて多くの書籍や情報が出ています。だけどそれと比較すると音の情報はかなり少ないのです。だから、Player10になることで、音に関するプログラムをする人が増えたら面白そうだと思ったのです。
私自身、ついこの間サウンドプログラミングの世界を知ったばかりなので、大したことはかけないんですが、興味を持つ人が増えたらいいなと思います。人が増えることで「Flashで作ろう!シンセサイザー」みたいな本が出たら楽しいじゃないですか。というかその本欲しい!

なのでTenoranの解析は裏でちょろちょろとやりながら、Player10での音生成などを含めて「初心者だけどPlayer10でサウンドプログラミングやろう!」みたいなのを何回かに分けて書いてみようと思ってます。

ちなみに今回のソースはこちら。

package
{
  import __AS3__.vec.Vector;

  import fl.controls.Button;
  import fl.controls.ComboBox;
  import fl.controls.Label;

  import flash.display.Graphics;
  import flash.display.Sprite;
  import flash.display.StageAlign;
  import flash.display.StageScaleMode;
  import flash.events.Event;
  import flash.events.MouseEvent;
  import flash.events.SampleDataEvent;
  import flash.media.Sound;
  import flash.media.SoundChannel;
  import flash.text.TextFieldAutoSize;

  [SWF(width="410", height="400", frameRate="30", backgroundColor="#b6c3be")]
  public class SoundSampleAnalyze extends Sprite
  {
    private var bg:Class;
    private var phase:Number = 0;
    private var frequency:Number = 440;
    private var shapeFactor:Number = 0;
    private var pi2:Number = Math.PI * 2;
    private var graphBase:Sprite;
    private var graph:Sprite;
    private var startStopButton:Button;
    private var isPlaying:Boolean;
    private var ch:SoundChannel;
    private var mysound:Sound;
    private var label:Label;
    private var combo:ComboBox;
    private var sampleType:String;

    public function SoundSampleAnalyze()
    {
      stage.scaleMode = StageScaleMode.NO_SCALE;
      stage.align = StageAlign.TOP_LEFT;
      initializeGraph();
      initializeButton();
      initalizeLabel();
      initializeCombobox();
      sampleType = "C";

      //Soundインスタンスを作る
      mysound = new Sound();
      mysound.addEventListener("sampleData", onSampleData);
      stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
      isPlaying = false;
    }

    private function onSampleData(e:SampleDataEvent):void{
      var sig:Number;
      var samples:Vector.<Number> = new Vector.<Number>();

      for (var i:int = 0; i < 2048; ++i )
      {
        sig = getCurrentData() * 0.7;
        samples.push(sig);
        //SampleDataEvent.dataに波形データを書き込む(-1.0~1.0のNumber)
        //左チャンネル
        e.data.writeFloat(sig);
        //右チャンネル
        e.data.writeFloat(sig);
      }
      drawWaveForm(samples, graph, 380, 280);
    }

    private function getCurrentData():Number
    {
      var sample:Number;
      phase += frequency * pi2 / 44100;
      if (phase > pi2) phase -= pi2;
      switch(sampleType)
      {
        case "A":
          sample = Math.sin(phase) * shapeFactor;
        break;

        case "B":
          sample = (phase < Math.PI ? 1 : -1 ) * (1 - shapeFactor) * 0.5;
        break;

        case "C":
          sample = Math.sin(phase) * shapeFactor + (phase < Math.PI ? 1 : -1 ) * (1 - shapeFactor) * 0.5;
        break;

        default:
        break;
      }

      return sample;
    }

    private function mouseMoveHandler(e:MouseEvent):void{
      frequency = graphBase.mouseY * 2  + 55;
      shapeFactor = 1 - graphBase.mouseX / graphBase.width;
      label.text = "shapeFactor: " + shapeFactor + "nfrequency: " + frequency;
    }

    private function drawWaveForm(data:Vector.<Number>, target:Sprite, WIDTH:uint, HEIGHT:uint):void
    {
      var g:Graphics = target.graphics;
      var i:uint;
      var len:uint = data.length;
      var rate:Number = WIDTH / len;
      var step:Number = (rate < 1) ? (1 / rate) : 1;
      var HALF:uint = HEIGHT / 2;
      g.clear();
      g.beginFill(0x000000, 1);
      g.drawRect(0, 0, graphBase.width, graphBase.height);
      g.endFill();
      g.lineStyle(1, 0xff0000, 1);
      g.moveTo(0, HALF);
      for(i = 0; i < data.length; i += step)
      {
        g.lineTo(i * rate, HALF - HEIGHT * data[i] * 0.7);
      }
    }

    private function initializeGraph():void
    {
      var g:Graphics;
      var devideNum:uint = 10;
      var i:uint;
      var devideWidth:Number;
      var HEIGHT:uint;
      var samplingRate:uint = 44.1;
      graphBase = new Sprite();
      addChild(graphBase);
      graph = new Sprite();
      graphBase.addChild(graph);
      g = graphBase.graphics;
      g.beginFill(0x000000, 1);
      g.drawRect(0,0,380,280);
      g.endFill();
      graphBase.x = 15;
      graphBase.y = 10;
    }

    private function initalizeLabel():void
    {
      label = new Label();
      addChild(label);
      label.move(200, 300);
      label.text = "";
      label.textField.autoSize = TextFieldAutoSize.LEFT;
    }

    private function initializeButton():void
    {
      startStopButton = new Button();
      startStopButton.width = 160;
      startStopButton.move(10,300);
      startStopButton.label = "スタート";
      addChild(startStopButton);

      startStopButton.addEventListener(MouseEvent.MOUSE_DOWN, startStopButtonDownHandler);
    }

    private function initializeCombobox():void
    {
      combo = new ComboBox();
      combo.addItem( { label: "式 A", data:"A" } );
      combo.addItem( { label: "式 B", data:"B" } );
      combo.addItem( { label: "式 A + B", data:"C" } );
      combo.selectedIndex = 2;
      addChild(combo);
      combo.move(10, 330);
      combo.addEventListener(Event.CHANGE, comboChangeHandler);
    }

    private function comboChangeHandler(e:Event):void
    {
      sampleType = combo.value;
    }

    private function startStopButtonDownHandler(e:MouseEvent):void
    {
      isPlaying = !isPlaying;
      var label:String = isPlaying ? "ストップ" : "スタート";
      startStopButton.label = label;
      if(isPlaying)
      {
        ch = mysound.play();
      }
      else
      {
        ch.stop();
      }
    }
  }
}

自作iPhoneアプリ 好評発売中!
フォルメモ - シンプルなフォルダつきメモ帳
ジッピー電卓 - 消費税や割引もサクサク計算!

ページトップへ戻る