ねとらじステーション解体新書

シャオ志向で好評公開中のねとらじステーション。
別に要望があった訳ではありませんが覚え書き程度に情報を公開します。
あなたのお役に立てましたら幸いです。
(Version 1.35現在)


目次


開発環境
 
OS Windows XP Home Edition SP2
CPU Athlon 64 Processor 3500+ (2.2GHz)
Memory DDR-SDRAM 1GB
Graphics ATI RADEON XPRESS 200 Series 1024x768 32bit
Sound Realtek High Definition Audio
Network Yahoo!BB 8M (Survey: Down=2.8Mbps, Up=509Kbps)
Develop Env. Delphi 7 Professional

使用している非ビジュアルコンポーネント
 
コンポーネント名 作者 用途
TWaveFFT ヒロユキ 音声スペクトル分析
TActionManager Borland メニュー管理
TXPColorMap Borland XP風メニュースタイル
TSaveDialog Borland 未使用
TPopupMenu Borland BGMオープン
TIdTcpClient Indy Project サーバとの通信用
TTimer Borland プログレスバー更新
送受信状態監視
自動放送開始
メーター更新
カレントステータス更新
自動放送メイン
TOpenDialog Borland BGMオープン
TGogoTea M.yanagisawa さん mp3エンコード
TKThdComp こける さん mp3取り出し・データ送信
カレントステータス取得
TIdEncoderMIME Indy Project パスワード符号化用
TActionList Borland アクション管理
TFindFile M&I さん 効果音検索
TDDSD Taku Hayase さん 音声取り込み
TIdLogEvent Indy Project 送信データ量監視
TMixer Vivas さん ミキサーのコントロール
TApplicationEvents Borland 例外捕捉
ホットキー処理
スレッド開始
TIdHTTP Indy Project 番組リスト取得
TImageList Borland タブ用アイコン

メイン処理のフローチャート

フローチャート
通信方法

 サーバとは、ICECAST2プロトコルを用い、TCPで接続します。手順を以下に示します。
  1. サーバに接続する(Connect)
  2. 空の文字列を送る(Write null)
  3. ヘッダを送る(Write)
    名前 値の意味 デフォルトまたは規定値
    太字:関数、斜体:パラメータ)
    備考
    Content-Type 送るデータの種類 audio/mpeg  
    Authorization Base64にエンコードしたパスワード Basic Base64("source:パスワード") パスワードは今のところ"ladio"
    ice-name ラジオ名   表示にのみ使われる
    ice-url 関連URL  
    ice-genre ジャンル(説明)  
    ice-bitrate ビットレート(kbps単位の数値)  
    ice-description ラジオ名  
    ice-public 0  
    ice-audio-info 再生情報 ice-samplerate=サンプルレート(Hz);ice-bitrate=ビットレート(kbps);ice-channels=チャネル数 これを間違うと再生できない

     名前と値は":"(コロン)で区切る。
     
    見本(プレーンテキスト)
    SOURCE /mountpoint ICE/1.0
    Content-Type:audio/mpeg
    Authorization:Basic c291cmNlOmxhZGlv
    ice-name:YKK夜のラジオDX
    ice-url:http://www.plusradi.com/
    ice-genre:とにかく喋ります。時々唄います。
    ice-bitrate:128
    ice-public:0
    ice-description:YKK夜のラジオDX
    ice-audio-info:ice-samplerate=22050; ice-bitrate=128; ice-channels=1

  4. 空白行を送る(Writeln)
  5. レスポンスを待つ(ReadlnWait)
  6. 以降はmp3データをWriteしていく
  7. 終了するときはいきなりDisconnectしてよい
接続時のひな形
procedure TForm1.StartBroadcast;
var
      Header : TStringList;
      Password : String;
begin
      // エラーチェック
      if マウントポイント = '' then
      begin
            raise Exception.Create('マウントポイントを入力してください');
      end;

      Header := TStringList.Create;
      try
            // クライアントの設定
            IdTcpClient1.Host := サーバー;
            IdTcpClient1.Port := ポート;
            Password := IdEncoderMIME1.EncodeString('source:'+パスワード);

            // ヘッダの設定
            Header.NameValueSeparator := ':';
            with Header do
            begin
                  Add(Format('SOURCE %s ICE/1.0',[マウントポイント]));
                  Values['Content-Type'] := 'audio/mpeg';
                  Values['Authorization'] := 'Basic ' + Password;
                  Values['ice-name'] := ラジオ名;
                  Values['ice-url'] := 関連URL;
                  Values['ice-genre'] := ジャンル;
                  Values['ice-bitrate'] := ビットレート;
                  Values['ice-public'] := '0';
                  Values['ice-description'] := ラジオ名;
                  Values['ice-audio-info'] := Format(
                                                'ice-samplerate=%d;ice-bitrate=%d;ice-channels=%d',[
                                                      サンプリングレート,
                                                      ビットレート,
                                                      チャネル数
                                                ]
                                              );
            end;

            // 接続
            IdTCPClient1.Connect();

            // ヘッダ送信
            IdTCPClient1.Write('');
            IdTCPClient1.Write(Header.Text);
            IdTCPClient1.WriteLn;

            // レスポンスを待つ
            IdTCPClient1.ReadLnWait;
      finally
            Header.Free;
      end;

end;
デバイスのリストアップ

 複数のデバイスに対応できるよう、DirectSoundCaptureEnumerate手続き(dsound.pas)を呼びます。コールバック関数では、GUIDとデバイス名が渡されますので、それをどこかに記憶しておきます。

手続きのひな形
DirectSoundCaptureEnumerate(lpDSEnumCallback(コールバック関数へのポインタ),nil);

コールバック関数のひな形
function DSEnumProc(lpGUID:PGUID; lpszDesc,lpszDrvName:PChar;lpContext:Pointer):Boolean; stdcall;
var
      DeviceName : String;
      DeviceGUID : PGuid;
begin
      DeviceName := lpszDesc;

      if lpGUID = nil then
      begin
            DeviceName := 'プライマリデバイス';
            DeviceGUID := nil;
      end else
      begin
            // メモリを確保してGUIDを保存。ソフト終了時にFreeMemで解放すること
            GetMem(DeviceGUID, sizeof(TGuid));
            DeviceGUID^ := lpGuid^;
      end;

      // ここでGUID(へのポインタ)とデバイス名をセットにして保存。コンボボックスに保存する例は↓
      //Form1.ComboBox22.Items.AddObject(DeviceName, TObject(DeviceGUID));

      // 戻り値はtrue
      Result := true;
end;
 使用しているデバイスを変更するためには、TDDSDCaptureを少し改造する必要があります。

ddsd.TDDSDCapture.Create
//コンストラクタ
constructor TDDSDCapture.Create(OWner: TObject; Device:DSound.PGuid=nil);
var
      ret : Cardinal;
begin
  FOwner:=Owner;
  FLength:=0;
  FOnRecording:=Nil;

  //DirectSoundCaptureオブジェクトの生成
  ret := DirectSoundCaptureCreate8(Device, FDSCapture, Nil);
  if ret <> DS_OK then begin
    FInitialized:=False;
    exit;
  end;

  //初期化はOK
  FInitialized:=True;

end;
 変更したTDDSDCaptureを使って、次のようにデバイスを変更します(例)。
Capture.OnRecording := nil;
Capture.Stop;
Capture.Free;

Capture := TDDSDCapture.Create(DDSD1, PGuid(Pointer to GUID);
Capture.SetupCaptureBuffer(44100,16,Channels=2,NUM_SAMPLE*Channels*2);
Capture.OnRecording := Captured;
Capture.StartLoop;
番組リストの取得と判断

 番組リストを取得し、リストに載っているかどうかチェックします。リスト自体は、http://yp.ladio.livedoor.jp/stats/list.dat に置かれており、それをTIdHTTPコンポーネントで取得します。取得したものをパースし、ポート番号・マウントポイントが一致するラジオがあった場合、リストに載っていると判断します。

リストの取得とチェックサンプルコード
procedure GetAndCheck();
var
      Lines : TStringList;
      Block : TStringList;
      i,j : Integer;
      InBlock : Boolean;
      c : Integer;
      Flag : Boolean;
begin
      Lines := TStringList.Create;
      Block := TStringList.Create;
      try
            try
                  Lines.Text := IdHttp1.Get('http://yp.ladio.livedoor.jp/stats/list.dat');
            except

            end;

            // 初期化
            Flag := false;

            InBlock := false;
            c := 0;
            for i := 0 to Lines.Count-1 do
            begin
                  if (Lines[i] = '') then continue;

                  if (Lines[i] = '') then
                  begin
                        if (not InBlock) then continue
                        else
                        begin
                              // ここでBlock.Values['キー名'] としてラジオ情報を取得できる
                              if StrtoInt(Block.Values['prt']) = ポート then
                              begin
                                    if Block.Values['mnt'] = マウントポイント then
                                    begin
                                          // 載っていた
                                          Flag := true;
                                          break;
                                    end;
                              end;

                              Inc(c);

                              InBlock := false;
                              continue;
                        end;
                  end;

                  if (not InBlock)and(Lines[i] <> '') then
                  begin
                        InBlock := true;
                        Block.Clear;
                  end;

                  if InBlock then Block.Add(Lines[i]);

            end;

            if not Flag then
            begin
                  // 載っていなかった
            end;

      finally
            Lines.Free;
            Block.Free;
      end;

end;

TGogoTeaを用いたエンコードと送信

 TGogoTeaを用いてエンコードしながらmp3データを送信します。TGogoTeaのEncodeTypeは_MEMORYにしておき、入力はメモリから、出力はファイルへとします。このファイルがキュー兼キャッシュとなります。TGogoTeaにWaveを送り込み、出力ファイルのサイズが増えた分だけメモリに読み込みます。それが送るべきmp3データとなります。ただし、これだとキャッシュが無限大に膨れあがるため、リミットを設けてリセット処理を入れておきます。

実際の処理(そのまま抜き出し)
procedure TForm1.KThdComp1Execute(Sender: TObject);
const
      MAX_SIZE = 100*1024*1024;    // 100MB
var
      Buffer : PChar;
      Buffer2 : PByte;
      Reader : TFileStream;
      TrueRead : Integer;
      ResetFlag : Boolean;
      Size : Integer;
      DontReset : Boolean;
begin
      if Exclusive then exit;

      Size := CopySize;
      DontReset := false;

      Exclusive := true;
      GetMem(Buffer,Size);
      GetMem(Buffer2,Size);
      try
            KThdComp1.Synchronize(Integer(Buffer),0);
            //BlockCopy(Capture,StartOffset, Buffer, Size);

            // フィルタと音量
            FilterWave(
                  Pointer(Buffer),
                  Size div sizeof(T16bitSound),
                  SignalL,
                  SignalR,
                  StrtoInt(ComboBox2.Text) div 2
            );

            // リサンプラ
            Size := Resampler(Pointer(Buffer), Size div sizeof(T16bitSound)) * sizeof(T16bitSound);

            // バッファ書き込み
            Gogotea1.WriteBuffer(Buffer, Size);

            // ストリーム読み込み
            Reader := TFileStream.Create(DestFile,fmOpenRead or fmShareDenyNone);
            try

                  // 新規のデータを取り出す
                  Reader.Position := mp3pos;
                  TrueRead := Reader.Read(Buffer2^, Size);
                  IdTcpClient1.WriteBuffer(Buffer2^,TrueRead);

                  // 既定の容量を超したらリセット
                  ResetFlag := false;
                  if Reader.Size > MAX_SIZE then ResetFlag := true;

            finally
                  mp3pos := Reader.Position;
                  Reader.Free;
            end;

            if (ResetFlag)and(not DontReset) then
            begin
                  // エンコード中断
                  Gogotea1.WriteBuffer(nil,0);
                  Gogotea1.AbortEncode;
                  while Gogotea1.IsEncoding do sleep(10);

                  // キャッシュ削除
                  DeleteFile(DestFile);
                  mp3pos := 0;

                  // 再実行
                  Gogotea1.QuerySettings;
                  Gogotea1.Execute;
            end;

      finally
            Exclusive := false;
            FreeMem(Buffer);
            FreeMem(Buffer2);
      end;
end;
ダウンロード

Unit1.pas (63KB)
FIRLPF.pas (15KB)
 

更新履歴
2007/9/17(Mon) 書き下ろし
Written by Hiroyuki Watanabe.