Network.VoiceText と Sound.ALUT で音声データを再生する

以前作った[VoiceText Web API](https://cloud.voicetext.jp/webapi)をHaskellから呼び出すためのライブラリを使って、Haskellプログラムで音声データの再生を試してみた。

* GitHub: [Network.VoiceText](https://github.com/zaneli/network-voicetext)
* Qiita: [VoiceText Web API の Haskell 用ライブラリを作った](http://qiita.com/zaneli@github/items/ee6fcabb264b723b087f)

[Sound.ALUT](https://hackage.haskell.org/package/ALUT)というのを使うとよさそう。

# インストール

Sound.ALUT と 拙作 Network.VoiceText を cabal sandbox 環境にインストールする。

## cabal sandbox 環境のセットアップ

```
> mkdir voicetext
> cd voicetext
> cabal sandvox init
> cabal init
> vi voicetext.cabal
```

voicetext.cabal はこんな感じに編集する。(一部抜粋)

```hs:voicetext.cabal
executable voicetext
  main-is:        Main.hs
  build-depends:  base >=4.7 && <4.8
                , network-voicetext >= 0.0
                , bytestring >=0.10
                , OpenAL >=1.6
                , ALUT >=2.3
```

## 依存ライブラリのインストール

```
> cabal install network-voicetext-0.0.0.1.tar.gz
> cabal install --only-dependencies
Resolving dependencies...
Notice: installing into a sandbox located at
/Users/zaneli/ws/haskell/voicetext/.cabal-sandbox
Downloading OpenAL-1.6.0.1...
Configuring OpenAL-1.6.0.1...
Building OpenAL-1.6.0.1...
Installed OpenAL-1.6.0.1
Downloading ALUT-2.3.0.2...
Configuring ALUT-2.3.0.2...
Failed to install ALUT-2.3.0.2
Last 10 lines of the build log ( /Users/zaneli/ws/haskell/voicetext/.cabal-sandbox/logs/ALUT-2.3.0.2.log ):
Configuring ALUT-2.3.0.2...
setup-Cabal-1.18.1.4-x86_64-osx-ghc-7.8.3: Missing dependency on a foreign
library:
* Missing C library: alut
This problem can usually be solved by installing the system package that
provides this library (you may need the "-dev" version). If the library is
already installed but in a non-standard location then you can use the flags
--extra-include-dirs= and --extra-lib-dirs= to specify where it is.
cabal: Error: some packages failed to install:
ALUT-2.3.0.2 failed during the configure step. The exception was:
ExitFailure 1
```

むむ、Cのライブラリが無いと言われて失敗した。
まず alut を入れる。Mac の場合、brew で入れられた。

```
> brew update
> brew install alut
==> Installing freealut
==> Downloading http://connect.creativelabs.com/openal/Downloads/ALUT/freealut-1.1.0.tar.gz
Already downloaded: /Library/Caches/Homebrew/freealut-1.1.0.tar.gz
==> Patching
patching file configure.ac
==> ./autogen.sh
==> ./configure --prefix=/usr/local/Cellar/freealut/1.1.0 --mandir=/usr/local/Cellar/freealut/
==> make install
?  /usr/local/Cellar/freealut/1.1.0: 11 files, 180K, built in 27 seconds
```

もう一度 `cabal install --only-dependencies` したが同じエラー。
--extra-lib-dirs オプションで freealutインストールディレクトリを指定したところ、成功した。

```
> cabal install --only-dependencies --extra-lib-dirs=/usr/local/Cellar/freealut/1.1.0/lib
Resolving dependencies...
Notice: installing into a sandbox located at
/Users/zaneli/ws/haskell/voicetext/.cabal-sandbox
Configuring ALUT-2.3.0.2...
Building ALUT-2.3.0.2...
Installed ALUT-2.3.0.2
```

# 実行

## 音声ファイルを再生

今回の目的からは少し外れているけど、一旦ファイルを作って、それを再生するにはこういう風にする。

```hs:Main.hs
import Network.VoiceText
import Sound.ALUT
import System.Environment (getArgs)

main = playVoice

playVoice :: IO ()
playVoice = withProgNameAndArgs runALUTUsingCurrentContext $ \_ _ ->
  do
    message <- getArgs
    let tmpFileName = "./tmp.wav"
    ttsToFile tmpFileName (basicAuth "basic_auth_username" "") (ttsParams (head message) Show)
    (Just device) <- openDevice Nothing
    context <- createContext device []
    currentContext $= context
    buffer <- createBuffer $ File tmpFileName
    [source] <- genObjectNames 1
    queueBuffers source [buffer]
    play [source]
    sleep 1
    closeDevice device
    return ()
```

`cabal run "Hello, world"` とか実行すると、「はろ〜わ〜るど」とおしゃべりしてくれる。
(毎回tmp.wavファイルが作成されるが…)

## 音声データを再生

Network.VoiceText には音声ファイルを作成する `ttsToFile` 関数とは別に、レスポンスデータを ByteString として返す `tts` 関数を用意しているので、できればファイルを作らず ByteString を直接再生したい。

```hs:Main.hs
import Data.ByteString.Internal (toForeignPtr)
import Data.ByteString.Lazy (toStrict)
import Foreign.ForeignPtr (castForeignPtr)
import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)
import Network.VoiceText (basicAuth, tts, ttsParams, Speaker(Show))
import Sound.ALUT (
  ($=),
  closeDevice,
  createBuffer,
  createContext,
  currentContext,
  genObjectNames,
  openDevice,
  play,
  queueBuffers,
  runALUTUsingCurrentContext,
  sleep,
  SoundDataSource(FileImage),
  withProgNameAndArgs)
import System.Environment (getArgs)
import qualified Foreign.C.Types as CT
import qualified Sound.OpenAL.AL.Buffer as AL

main = do
  mr <- createVoice
  playVoice mr

createVoice :: IO (AL.MemoryRegion a)
createVoice = do
  [message] <- getArgs
  (Right bytes) <- tts (basicAuth "basic_auth_username" "") (ttsParams message Show)
  let (fptrw, _, ptrLength) = toForeignPtr $ toStrict bytes
  let fptri = castForeignPtr fptrw
  let size = CT.CInt $ fromIntegral ptrLength
  let ptr = unsafeForeignPtrToPtr fptri
  return $ AL.MemoryRegion ptr size

playVoice :: AL.MemoryRegion a -> IO ()
playVoice mr = withProgNameAndArgs runALUTUsingCurrentContext $ \_ _ -> do
  (Just device) <- openDevice Nothing
  context <- createContext device []
  currentContext $= context
  buffer <- createBuffer $ FileImage mr
  [source] <- genObjectNames 1
  queueBuffers source [buffer]
  play [source]
  sleep 1
  closeDevice device
  return ()
```

後述する参考URLを元に書いてみて、うまく動作することは確認できたが型の変換の辺りよく理解できていない…。

しかしこれで、Haskellとおしゃべりできたぞ！

# 参考URL
* [Haskell OpenAL Example](http://bluishcoder.co.nz/articles/haskell/openal.html)
* [audio - Play a wav file with Haskell - Stack Overflow](http://stackoverflow.com/questions/14005592/play-a-wav-file-with-haskell)
* [Ogg OpenAL :: lpaste — Lambda pastebin](http://lpaste.net/114338)