2015/10/17
JapanesePhoneticAnalyzerを使ってPowerShellで形態素解析(前編)
はじめに
がりっち氏がWindows10 UWPに日本語解析のAPIが備わっていた件 | garicchi.com というエントリを上げていました。
実はWin10に限らず、Win8.1 / Server 2012R2以降であれば、Windows ランタイム(WinRT)のWindows.Globalization名前空間に含まれるJapanesePhoneticAnalyzerクラスを用いた形態素解析ができます。
形態素解析とは要するに文字列を単語(正確には形態素という、文字列の最小構成要素)ごとに分割し、それぞれの単語の品詞を判別する処理になります。(JapanesePhoneticAnalyzerクラスだと分割までで、品詞の情報は取得できない?ようですが…)
またJapanesePhoneticAnalyzerでは分割した単語の読み仮名を取得することができます。
WinRTならPowerShellからも使えるんじゃないかなーと思ってやったらできたので、紹介します。
WinRTのPowerShellからの利用法
WinRTについての説明は他サイト様に譲りますが、要はWindowsストアアプリやUWP(ユニバーサルWindowsプラットフォーム)アプリを動作させる実行環境とAPI群です。
じゃあデスクトップアプリであるPowerShellは関係ないのかというとそうではなくて、例えばストアアプリのサイドローディングを行うAppxモジュールというものがあります。
つまりWinRTは従来のデスクトップアプリからの相互運用もできるようになっています。(すべてのコンポーネントではない)
このあたりの話は、荒井さんの記事が参考になるかと思います。:特集:デスクトップでもWinRT活用:開発者が知っておくべき、ライブラリとしてのWindowsランタイム (1/5) - @IT
PowerShellからWinRTを利用するには.NET Frameworkに含まれるクラスを利用するのと基本は同じです。
ただし注意点としては、クラス名を指定する時は、クラスの「アセンブリ修飾名」を指定する必要があります。
今回の例ではJapanesePhoneticAnalyzerクラスを使いますが、アセンブリ修飾名はWindows.Globalization.JapanesePhoneticAnalyzer, Windows.Globalization, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime
となります。
このうち必須となるのは
Windows.Globalization.JapanesePhoneticAnalyzer:クラスの完全修飾名
Windows.Globalization:クラスの含まれる名前空間
ContentType=WindowsRuntime:WinRTのコンポーネントであること
の3つだけのようです。
つまり、PowerShellからは
[Windows.Globalization.JapanesePhoneticAnalyzer, Windows.Globalization, ContentType=WindowsRuntime]
とすればクラスを参照することができます。
ちなみに任意の型のアセンブリ修飾名を知るには、[型名].AssemblyQualifiedName のようにすればOKです。
なお、WinRTにはPowerShellからも利用価値の高いクラスが他にも色々あるようです。ぎたぱそ氏が認証系のクラスについて書かれていますので、参考にしてみてください。:PowerShell も Windows Store Apps 同様に Windows.Security.Credentials namespace を使って認証情報を管理できるようにしてみる - tech.guitarrapc.com
単語を分割する
早速、形態素解析をやってみましょう。具体的にはJapanesePhoneticAnalyzerのGetWordsメソッドを呼び出すだけです。
すべての基本になるので軽く関数としてラップしておきます。
function Get-JpWord { param( [parameter(ValueFromPipeline=$true)] [ValidateLength(1,99)] [string[]] $Text, [switch] $MonoRuby ) process { foreach($t in $Text) { [Windows.Globalization.JapanesePhoneticAnalyzer, Windows.Globalization, ContentType=WindowsRuntime]::GetWords($t, $MonoRuby) } } }
GetWordsメソッドはスタティックメソッドなので、::演算子で呼び出します。戻り値はIReadOnlyList<JapanesePhoneme>というコレクションです。
GetWordsメソッドの第2引数にTrueを指定すると、漢字の含まれた単語をルビの振れる最小単位にまで分割する、Mono Rubyモードが有効になります。
なお、GetWordsメソッドはどうも文字数制限があるようです。だいたい100文字を超えると何も出力しない感じです。この制限値はリファレンスに書いてないようなので詳細不明ですが、一応関数では99文字までという制限を入れておきました。
例えば、Get-JpWord "最近急に寒くなってきました。" のようにすると結果は以下のように表示されます。
DisplayText IsPhraseStart YomiText ----------- ------------- -------- 最近 True さいきん 急に True きゅうに 寒くな True さむくな って False って き True き ました False ました 。 True 。
出力されるJapanesePhonemeオブジェクトは3つのプロパティを持ちます。
DisplayText | 分割された単語 |
IsPhraseStart | 単語が文節の開始であるかどうか |
YomiText | 単語の読み仮名 |
このように、入力した文章を単語単位に分割し、それぞれの単語の読み仮名を取得することができます。ただこれだけだと、「で、どうしろと」という感じなので、この出力結果を利用する、より実用的な関数を書いていきましょう。
長くなったので次回に続く。
2014/12/11
PowerShellでMMLシーケンサーを作ってみた(その1)
MMLとは
この記事はPowerShell Advent Calendar 2014の11日目の記事です。
突然ですが、MMLってご存知ですか? MMLとはMusic Macro Languageの略で、その名の通り音楽演奏データを記述するための言語です。BASICのPLAY文で使うあれです。MIDIファイルに変換するコンパイラもありましたよね。
たとえば、「ねこふんじゃった」の最初の部分をMMLで書くとこんな感じです。MMLはいろんな記法があるのですが、よくあるのはこういうのです。
e-d-<g-4>g-rg-4e-d-<g-4>g-rg-4e-d-<g-4>g-4<e-4>g-4<d-4>frf4 e-d-<d-4>frf4e-d-<d-4>frf4e-d-<d-4>f4<e-4>f4<g-4>g-rg-4
ドの音をc、レの音をd、のように以下シの音をbまでアルファベット音階で割り当てます。半音上げる(シャープ)場合は音名のあとに+、半音下げる(フラット)場合は-を付与します。音の長さは4分音符なら4、8分音符なら8と書きます。付点は数字のあとに.を付けます。数字省略時は8分音符と解釈するのが普通のようです。オクターブを上げる場合は>、下げる場合は<と書きます(コンパイラによっては逆の解釈をする)。休符はrです。
本物のMMLでは、音色の指定だとかトラックの割り当てだとか、もっとたくさん命令があるんですが、基本はだいたいこんなもんです。
PowerShellで音を鳴らすには
PowerShellで音を鳴らすには、.NETのSystem.Console.Beepメソッドを使うのが一番手っ取り早いです。ビープ音ですね。比較的古いWindowsではハードウェアのビープスピーカーから、比較的新しいWindowsではサウンドデバイス上で鳴ります。今回のMMLシーケンサーも結局はBeepメソッドです。
そしてぎたぱそ先生がずっと以前に書いておられます。なので今回のはまあ、MMLシーケンサー部分を除けば二番煎じなんですけどもね。(そしてSmallBasicLibraryを使えば実はMML演奏もできるので、車輪の再発明でもあるんですが)
ちなみにwavやmp3なんかを鳴らすには、Windows Media PlayerをCOM経由で実行する方法や.NET(WPF)のSystem.Windows.Media.MediaPlayerクラスを使う方法などがあります。
音程の決め方
Beepメソッドでは第1引数にビープ音の周波数をHzで、第2引数に再生時間をミリ秒で指定します。つまりは音程(音の高さ)はHzで指定する必要があります。ここでドの音は何ヘルツで、みたいな対応表をどこかから探してきてそのデータを使ってもいいんですが、今回はせっかくなので、PowerShellで計算して求めてみます。
まず、ある音程の一つ上のオクターブの音程の間の周波数比は、1:2です。
ピアノ等の鍵盤楽器が近くにあれば、それを見てもらえば分かると思いますが、1オクターブには12の音程が含まれています。1オクターブの周波数比を12個均等に分割します(この周波数の決め方を平均律といいます)。つまり、隣り合う音程の周波数比は、1:12√2となるわけです。
さて、音程の周波数比はこれでわかりました。あとは基準となる音の周波数が分かれば、全ての音の周波数が計算できるわけです。時代や地域、演奏する楽器等によって微妙に違ってきますが、現代日本においては普通は、A(ラ)=440Hzが基準周波数です。
あとは単純にかけ算して周波数を求めて連想配列に入れておくだけです。コードにすればこんな感じですね。
$baseFrequency = 440.0 $pitches = @{ "c" = $baseFrequency * [math]::Pow(2, 3/12) "d" = $baseFrequency * [math]::Pow(2, 5/12) ... }
あと、+で半音上がる場合は[math]::Pow(2, 1/12)をかけ、-で半音下がる場合は割ればいいだけです。(別に$pitchesテーブルで最初から定義してもいいかもですが)
音価の決め方
音価というのは音の長さです。Beepメソッドの第2引数にはミリ秒で実際の時間を指定する必要があるので、これも計算で求めてやります。
4分音符というのは、1小節(全音符)を4分割、つまり1/4した音価を持ちます。同様に8分音符は1/8ですね。付点がつくと音価は1.5倍になります。
ではたとえば4分音符の具体的な音価はどうやって定めるのでしょうか。それを決めるのがテンポと拍子です。
テンポは1分間(つまり60000ミリ秒)の拍数(BPM、Bit Per Minuteともいう)で定義されます。今回作るシーケンサーは4/4拍子、すなわち1拍=4分音符とする4拍子固定とするので、1分間に4分音符がBPM回含まれることになります。
つまり、4分音符の音価=60000ミリ秒/BPMの値、全音符の音価=60000*4ミリ秒/BPMの値 となるわけです。
具体的なコードは…略します。単なるかけ算と割り算だけなんで。
MMLのパース
えっと時間がなくなってきたので、駆け足で説明します。
MMLのパースとは、要するに最初の例のようなMML(テキストデータ)から、実際に演奏する音程と音価の組み合わせのデータとして組み立てる作業になります。
今回のシーケンサーでは、繰り返し記号等はサポートしないので、単にMMLを一文字ずつ読み込んで、音名(cなど)を見つけたら、その後の文字を読んで、+があれば半音上げるなどして周波数を求め、その後に数字があればn分音符とみなし、先ほどの計算式から実際の音価を求めてやる。というのを全部の音でくり返すだけです。
この処理をやっているのがConvertFrom-MMLという関数です。MMLテキストを入力してやると、周波数(Frequency)や発音時間(Duration)などをプロパティとして含んだpscustomobject(Noteオブジェクト)を出力します。(Noteとは音符のことです)
MMLの演奏
パースしたMMLを実際に演奏するのがInvoke-Beep関数です。入力は、ConvertFrom-MMLで生成したNoteオブジェクトです。実際の処理は至って単純で、 [System.Console]::Beep($note.Frequency, $note.Duration)するだけです(えー)。あ、あと休符の時はDuration分だけSleepを入れてやっています。
MMLの>と<の意味を逆転する-Reverseスイッチ、BPMを指定する-Bpmパラメータ(デフォルト120)なんかも用意してます。
実際にはMMLから直接演奏できるように、Invoke-Mmlというラッパー関数を用意しています。
冒頭のねこふんじゃったのMMLを再生するにはこんな感じにします。
$s=@" e-d-<g-4>g-rg-4e-d-<g-4>g-rg-4e-d-<g-4>g-4<e-4>g-4<d-4>frf4 e-d-<d-4>frf4e-d-<d-4>frf4e-d-<d-4>f4<e-4>f4<g-4>g-rg-4 "@ Invoke-Mml $s
コード
function ConvertFrom-MML { [CmdletBinding()] param( [parameter(ValueFromPipeline=$true)][string[]]$mml, [int]$bpm = 120, [switch]$reverse = $false ) begin { $baseFrequency = 440.0 $scaleRatio = [math]::Pow(2, 1/12) $baseDuration = 60000 * 4 / $bpm $pitches = @{ "c" = $baseFrequency * [math]::Pow(2, 3/12) "d" = $baseFrequency * [math]::Pow(2, 5/12) "e" = $baseFrequency * [math]::Pow(2, 7/12) "f" = $baseFrequency * [math]::Pow(2, 8/12) "g" = $baseFrequency * [math]::Pow(2, 10/12) "a" = $baseFrequency * [math]::Pow(2, 12/12) "b" = $baseFrequency * [math]::Pow(2, 14/12) "r" = -1 } $currentOctave = 0 } process { $chars = [string[]]$mml.ToCharArray() 0..($chars.Length - 1)|%{ if($null -ne $pitches[$chars[$_]]) { $frequency = $pitches[$chars[$_]] $suffix = "" $denominator = "" $dot = $false if($_ -lt $chars.Length-1) { switch($chars[($_ + 1)..($chars.Length - 1)]) { {$pitches.Contains($_)} {break} "+" {$frequency *= $scaleRatio; $suffix += $_} "-" {$frequency /= $scaleRatio; $suffix += $_} {$_ -match "\d"} {$denominator += $_} "." {$dot = $true} } } $frequency *= [math]::Pow(2, $currentOctave) $denominator = [int]$denominator if($denominator -eq 0){$denominator = 8} $multiplier = 1 / $denominator if($dot){$multiplier *= 1.5} $duration = $baseDuration * $multiplier if($frequency -le 0){$on = $false}else{$on = $true} [pscustomobject]@{ Frequency = [int]$frequency Duration = $duration Note = "$($chars[$_])$suffix$denominator$(if($dot){"."})" Octave = $currentOctave Pitch = $chars[$_] Suffix = $suffix Denominator = "$denominator$(if($dot){"."})" On = $on } } elseif($chars[$_] -eq ">") { if($reverse) { $currentOctave-- } else { $currentOctave++ } } elseif($chars[$_] -eq "<") { if($reverse) { $currentOctave++ } else { $currentOctave-- } } } } } function Invoke-Beep { [CmdletBinding()] param([parameter(ValueFromPipeline = $true)][psobject[]]$note) process { if($note.On) { [System.Console]::Beep($note.Frequency, $note.Duration) } else { Start-Sleep -Milliseconds $note.Duration } } } function Invoke-Mml { [CmdletBinding()] param( [string]$mml, [int]$bpm, [switch]$reverse ) ConvertFrom-Mml @PSBoundParameters|Invoke-Beep }
おわりに
今回はMMLをパースしてBeepを演奏するスクリプトをPowerShellで作ってみました。これをシーケンサーというのはおこがましいと思いますが、まあたまにはこういう柔らかいネタもいいんじゃないでしょうか。ドレミの音がどうやって決められてるのかというのも、もしかして話のネタくらいにはなるかも。
そしてこれ、シーケンサーといいつつ、入力機能がないですよね。それについては次回やります。
PowerShellアドベントカレンダー2014はまだまだ残席ございます。ぜひ、あなたのPowerShell話を聞かせて下さい。
Copyright © 2005-2018 Daisuke Mutaguchi All rights reserved
mailto: mutaguchi at roy.hi-ho.ne.jp
プライバシーポリシー