最近のブログ

RDF(RSS 1.0) URL RSS URL ATOM URL

2017年02月18日

吉里吉里Z 開発 :: OpenSL ES と XAudio2

ループチューナ で指定されたループやラベル等吉里吉里 2/Z のサウンド部分は複雑なので、PC でデバッグできると効率が良い。
ただ、OpenSL ES と DirectSound は API 体系がだいぶ違うので共通化は大変。
XAudio2 は OpenSL ES とほとんど同じで、データがなくなるとコールバックで呼ばれ、バッファをキューに入れていく形になっている。
インターフェイスで実装を分離すれば、基本部分は共通化できるのでそうした。

せっかく実装したのだから、PC 版では XAudio2 か DirectSound かを選択できる形にしようと思い、XAudio2 についていろいろと調べてみたが手間がかかりそうなので、共通部分の開発用にのみ使用して、実環境では使わないことになりそう。
使うのなら、WASAPI で置き換える形が現実的。

投稿者 Takenori : 04:54

吉里吉里Z 開発 :: XAudio2

DirectX のバージョンによって 2.0 ~ 2.7 がある。最新版である 2.7 を対象とすれば問題ないと思われる。
Windows8 は 2.8、Windows10 は 2.9 となっている。
環境にある DLL の xaudio2_7.dll、xaudio2_8.dll、xaudio2_9.dll の内、新しいバージョンの DLL をロードすればよさそうであるが、それだけではうまく動かなさそうである。

ヘッダーファイルを見るとバージョンによって、構造体にメンバが増えていたり、インターフェイスにメソッドが増えていたり、メソッドにパラメータが増えていたりするので、別物として扱う必要がある。
2.8 と 2.9 はヘッダー共通なので、2.7 とそれより後の 2 パターンで使用するメソッドについてラッパーを書けば両バージョンに対応できなくはなさそうである。
dll から XAudio2Create 関数を得て、それ以降で得られるインターフェイスの IXAudio2 などを別物として扱えば、理屈上は両方に対応できるはずである。
もしくは、Windows7 以下を切り捨てるのなら、2.8 と 2.9 のヘッダー(インターフェイス/構造体)でそのまま使える。

Windows7 を切らないという選択を選ぶのであれば、namespace とファイル分離、Factory によって何とかするのが記述量一番少ないと考えられる。
以下のような実装。

namespace xaudio2_7 {
#include "xaudio2_7.h"
#include "xaudio2driver.h"
};
IXAudio2Driver* CreateXAudio2Driver7(HMODULE hDll){...}


namespace xaudio2_9 {
#undef _WIN32_WINNT
#define _WIN32_WINNT _WIN32_WINNT_WIN8
#include "xaudio2_9.h"
#include "xaudio2driver.h"
};
IXAudio2Driver* CreateXAudio2Driver9(HMODULE hDll){...}


extern IXAudio2Driver* CreateXAudio2Driver7(HMODULE);
extern IXAudio2Driver* CreateXAudio2Driver9(HMODULE);

IXAudio2Driver* pDriver = nullptr;
HMODULE hDll = LoadLibrary(L"xaudio2_9.dll");
if( !hDll ) {
  hDll = LoadLibrary(L"xaudio2_8.dll");
}
if( hDll ) {
  pDriver = CreateXAudio2Driver9( hDll );
} else {
  hDll = LoadLibrary(L"xaudio2_7.dll");
  if( hDll ) pDriver = CreateXAudio2Driver7( hDll );
}
return pDriver;

// ここでは放置しているが hDll は使い終わったら FreeLibrary する。
// IXAudio2Driver は自前のインターフェイス

xaudio2_7 と xaudio2_9 でファイルを分離するのは define で定数等定義されているから。
xaudio2driver.h 内に通常の実装を入れて include することで2回書くのを回避する。
xaudio2_7.h は、DirectX SDK のヘッダーをリネームして置いて、xaudio2_9.h は Windows SDK の方のヘッダー。

と書いたものの、ここまでして実装した方がいいかは疑問。
XAudio2 に比べれば複雑になるものの、DirectSound よりは記述量少なそうな WASAPI を選択する方が良さそうに思える。

投稿者 Takenori : 04:30

2017年02月12日

WebAssembly :: wasm と JavaScript の間の文字列(バイト列)の受け渡し

extern void print( const char* ); などとしてコンパイルしたものの wast ファイルを見ると const char* は i32 となっている。以前見たバイトコードの仕様では文字列型はなかった。
Emscripten ではヒープの実態は ArrayBuffer となっていたが、WebAssembly でもメモリは ArrayBuffer で確保されているよう。
Understanding the JS API の Memory で書かれている。
メモリは、wasm で import / export 可能とあるが、C からコンパイルした wast を見ると (export "memory" (memory $0)) となっている。
このメモリに対して直接値を書き込む、もしくは読み込むことでバイト列を読み書きできると書かれている。
上述の print 関数の const char* は i32 として JavaScript に渡されるが、この値は何かというと、export された memory のインデックスを表していた。
JavaScript 側で文字列終端チェックを省くために C 側で長さを渡すように extern void print( const char*, int ); とした場合、JavaScript 側では以下のようにすることで文字列を受け取ることができる。

function print( offset, length ) {
  var buffer = new Uint8Array(instance.exports.memory.buffer, offset, length);
  console.log( String.fromCharCode.apply(null,buffer) );
}

const char* は、instance.exports.memory のインデックスとなっているので、そこから文字列長分を uint8 配列として切り出し、文字列化している ( ASCII 文字列以外ではうまくいかないかもしれない )。
これで C 側から文字列(バイト列)をコンソールに出力する関数が呼び出せる。


JavaScript から C 側へ文字列(バイト列)を渡すのはもう少し手間がかかる。
C 側で以下のように定義する。

static const int memSize = 1024;
char inputmemory[memSize];
char* getInputMemoryStart() { return inputmemory; }
int getInputMemoryLength() { return memSize; }

この領域に JavaScript から文字列を書き込んでもらい、そこを読み取ることで C 側で文字列を受け取れる。
JavaScript 側は以下のようになる(長さチェックはしていないし、ASCII 以外は考慮していないので注意)。

function putString( str ) {
  var offset = instance.exports.getInputMemoryStart();
  var length = instance.exports.getInputMemoryLength();
  var buffer = new Uint8Array(instance.exports.memory.buffer, offset, length);
  for( var i = 0; i < str.length; i++ ) {
    buffer[i] = str.charCodeAt(i);
  }
  buffer[str.length] = 0;
}

このスクリプトで文字列を書き込んでもらった後に――

const char* text = inputmemory;
print( text, stringLength(text) );

として C 言語側でアクセスできる。


以上でとりあえずは文字列の受け渡しが出来る。
ただ、実際には実用性に欠ける。
ある程度の規模で使うとなると malloc を自作するだろうから、そこで確保したメモリのポインターを JavaScript に渡せば、そのメモリの前に管理構造がついているような実装にすれば、その確保されたメモリの長さもわかるため、JavaScript でアクセスできる範囲も知ることができる。
そうすれば実用上も問題はなくなる。
malloc していないメモリを渡してしまうと壊れるが。

投稿者 Takenori : 22:59

WebAssembly :: WebAssembly がデフォルトで有効化されるのはいつ?

問題なく進めば2017年の第一四半期にバイナリフォーマットの初期バージョンが固まって、ブラウザで順次有効化されるらしい。
Firefox は 2017年3月のバージョン52 で有効化が予定されているとか。

2017年4月以降に本格的に取り組むと変更の対応が少なそう。


以下の記事に書かれている。
コミュニティにフィードバックを求めるためのWebAssemblyのブラウザプレビュー
JavaScriptを補完するウェブ用の新バイナリフォーマット「WebAssembly」--ブラウザプレビュー段階に

投稿者 Takenori : 19:31

WebAssembly :: wasm に clang を使った場合に C 内で関数呼び出しする場合に出るエラー

extern void print( const char*, int );
int stringLength( const char* s ) {
  int len = 0;
  while( s[len] ) { len++; }
  return len;
}
int c=0;
int count(){
  const char* text = "Hello world!";
  print( text, stringLength(text) );
  return c++;
}

このように JavaScript から count() を読んで、その中で stringLength() を呼ぶようなプログラムを書くと "Uncaught RuntimeError: memory access out of bounds" などと出てうまく動かない。
ソースコードに何か問題があるのか?と言うとそうではなく、コンパイル時にスタックの指定が必要とのこと。
具体的には――

s2wasm count.s -o sample.wast --allocate-stack 1024

のように --allocate-stack オプションが必要になる。

投稿者 Takenori : 18:28

WebAssembly :: C/C++ を wasm に clang を使用してコンパイル、JavaScript から呼び出す

WebAssembly は、2017年第一四半期のどこかで一区切りつけて破壊的変更が行われる予定ということなので、それ以後またツールチェーンの更新が必要になる。
以下、clang を使う方法で書いているが、Emscripten を使う方が簡単な様子。

環境構築
WindowsでWebAssemblyの環境を整える で書かれているのと大体同じだけど、「sexpr-wasm-prototype」は「WABT」に代わっている。
コンパイルエラーは最新版 (2017/2/11 に clone) では発生しなかった。
後、ALL_BUILD.vcxproj を開くと書かれているが、開くのはソリューションファイル LLVM.sln/binaryen.sln/WABT.sln が本筋。
コンパイル手順として、clang→llc→s2asm→sexpr-wasm と書かれているが、これも clang→llc→s2asm→wast2wasm となる。
コマンドとしては以下。

clang -S -emit-llvm --target=wasm32 sample.c
llc sample.ll -march=wasm32
s2wasm sample.s -o sample.wast
wast2wasm sample.wast -o sample.wasm


実行環境
Firefox 51 だと wasm 有効にしても「failed to match binary version」とエラーが出て動かない。少し古い binaryen だと動くかもしれないが今回は無視。Chrome で試す。
Chrome 56 は動かせた。ただローカルに置いた html ファイルから wasm ファイルを XMLHttpRequest で読み込ませようとしたらセキュリティーエラーが出るのでそれを回避するオプションをつけて起動する。
Chrome のショートカットを作って、「--user-data-dir="%UserProfile%\AppData\Local\Google\Chrome\UserDataLocal" --disable-web-security」などの引数を追加して起動する。
--user-data-dir= でユーザーデータフォルダを指定しているが、デフォルトの User Data のままではセキュリティ無効が効かなかったので、別途フォルダ作ってそちらを使うようにした。
後、wasm をアドレスバーに chrome://flags/#enable-webassembly と入力して有効に変更する必要がある。
ユーザーデータフォルダを切り替えたら wasm の指定はデフォルトになっているので注意。


C で書いた関数の呼び出し
Understanding the JS API に大体書かれている。
fetch で wasm ファイルを読むように書かれているが、ローカル file:// だと動かないので、XMLHttpRequest を使う必要がある。
サーバーに置くのならこの辺り関係ないが、まずはローカルで色々と実験するだろうから注意が必要。
wasm ファイルが読み込めたら WebAssembly.compile でコンパイルして、new WebAssembly.Instance でインスタンス化。
インスタンス化されたら instance.exports.func(); などとして呼び出せる。

function instantiate(bytes, imports) {
  return WebAssembly.compile(bytes).then(m => new WebAssembly.Instance(m, imports));
}
var importObject = { env: { print : arg => console.log(arg) } };
var instance;
var xhr = new XMLHttpRequest();
xhr.open('GET', 'sample.wasm', true);
xhr.responseType = 'arraybuffer';
xhr.onload = function() {
  var binary = xhr.response;
  var binarray = new Uint8Array( binary );
  instantiate( binarray, importObject ).then( inst => instance = inst );
  document.getElementById( 'countup' ).addEventListener( 'click', function (){
    putString();
    this.value = instance.exports.count();
  }, false);
};
xhr.send(null);

このサンプルスクリプトでは id が countup のボタンが html に必要。
then と書かれているのは非同期処理の Promise と言う仕組みのため。
WebAssembly.compile は非同期処理で Promise を返すため、then で非同期処理が終わった後の処理を記述する。
arg => console.log(arg) などは JavaScript のラムダ式。


JavaScript の関数を C 側から呼び出す
wasm 側で import する関数は、WebAssembly.Instance の第二引数に namespace + 関数名 のオブジェクトを渡せばよいようだ。
{ "namespace" : { "function" : function } }; のようなオブジェクト。
S 式ではリンクのように import で関数に割り当てればいいようだが、C 言語側からの場合は単純に extern で宣言して呼び出せば env.function_name のような形で import する宣言が追加されていたので、{ "env" : { "function_name" : function } } と言う形でオブジェクトを作れば、その関数をすんなり呼び出せる。
関数の形式は wast (wasm のテキスト形式の S 式) ファイルの import 部分を見ればわかるから、その形で JavaScript で関数を書けばよい。

extern void print( int );
int c=0;
int count(){
  print( c );
  return c++;
}


整数引数で JavaScript の関数を呼び出すのは問題ないが、文字列(バイト列)を受け渡しするのはひと手間必要になる。

投稿者 Takenori : 18:01

2017年01月23日

吉里吉里Z 開発 :: Android版のサウンドの制限

Android の OpenSL ES 仕様上発生する Windows 版の現吉里吉里Z 制限とは異なる制限。
ソフトウェアで事前に変換することで(表面上の)制限をなくすことは可能ではあるが、初期は行わない予定。

以下、具体的な制限。

・ 8 ビット符号なしまたは 16 ビット符号付き
(浮動小数点データでの再生は、Android 5.0 以降でのサポート。Android 版は初期は実装しない予定)

・モノラルまたはステレオ
( Windows 版はさらに多チャンネルサポートしているが Android 版は非サポート)

・サンプルレート
8000,11025,12000,16000,22050,24000,32000,44100,48000Hz
( 48000Hz を推奨。推奨値は端末依存だが 48kHz が多い模様。レイテンシ低減に効くとドキュメントに記載)

・同時再生数 32
(ドキュメントでは object と言う表現なので、もしかしたらもう少し少ないかもしれない。また OpenMAX AL と共有とのことなので動画もこの数に含む。自前でミキシングすればこの上限は関係ないが、そこまで厳しい制限でもないため現状上限はこのままの予定)


Audio output latency に低遅延に関する情報がある。
デバイスの最適サンプリングレートとバッファサイズを使用して、無音出力して準備するというものであるが、そこまでは行わない予定。
リップシンクや UI でタップ音などで遅延を感じる可能性はある。
あまりにひどいということであれば対策を考えるが、現在は 48kHz を推奨し、48kHz の時はバッファサイズを推奨サイズの倍数にするというところまでの対策にとどめる。

投稿者 Takenori : 16:54

2016年12月21日

吉里吉里Z 開発 :: システムのパス

System クラスのプロパティで各種パスが取得できるが、これらのパスは OS 固有。
今まで Windows しかなかったので、Windows 固有のパスを基準に作られている。
Android 版では、完全に別にしてしまうか、ある程度互換性を持たせるかの選択肢があるが、可能な範囲で用途的に同等と思われるパスを割り当て、Android 固有のパスを増やす形にする。
Android 版では以下のようなパスを割り当てることを考えているが、外部ストレージに関してはマウントのチェックなどもあるので再考するかもしれない。
再考時はアプリのアンインストールで削除される、アプリリストのデータ削除やキャッシュ削除で削除される等の挙動も考慮する。

System.appDataPath = Context.getFilesDir() ex) /data/data/{パッケージ名}/files
System.dataPath = Context.getExternalFilesDirs() ex) /storage/emulated/0/Android/data/{パッケージ名}/files
System.exeName = Context.getPackageCodePath() ex) /data/app/apkname.apk
System.exePath = Context.getPackageCodePath() ex) /data/app/
System.personalPath = Context.getExternalFilesDirs()ex) /storage/emulated/0/Android/data/{パッケージ名}/files
System.savedGamesPath = 対応するものなし

System.exePath は、apk が置かれるパスが相当すると思うが、Windows と違って Android では取得する意味がないと思われる。
System.exeName も、apk のファイル名を含んだフルパスであるが、これもあまり意味がない。
一応、apk は zip なので、吉里吉里Z 本体で展開して中のファイルをあれこれと出来なくはないが、意味は薄い。

Android 固有のパスは色々あるので、また後日リストアップする。

投稿者 Takenori : 23:19

2016年12月20日

吉里吉里Z 開発 :: ストレージアクセスフレームワーク(SAF) のパス

・ディレクトリ選択 ( ACTION_OPEN_DOCUMENT_TREE ) の場合、URI で返ってくるのは、外部ストレージの時 content://com.android.externalstorage.documents/tree/primary: + フォルダパス。
・ファイル選択 ( ACTION_OPEN_DOCUMENT ) の場合は、content://com.android.externalstorage.documents/document/primary: + ファイルパス。

primary の後のコロンとスラッシュは文字列で取得するとエスケープされている。

ディレクトリ選択で取得した URI で DocumentFile.fromTreeUri を使って開き、DocumentFile.listFiles でファイルリストを取得した場合の URI はどうなるか?
「content://com.android.externalstorage.documents/tree/primary: + フォルダパス + document/primary: + ファイルパス」になる。
長い上に単純にファイル名を結合したものではなく、パス部分すべてが結合された状態になる。
DocumentFile.getUri で取得すると上記のようになる。
DocumentFile.getName でディスプレイ名を取得して、/ を間に入れて無理やり文字列結合した場合 DocumentFile.isDocumentUri で false となる。
ディスプレイ名を使って URI を取得したいときは、ツリーの DocumentFile に対して DocumentFile.findFile で検索する必要があるようだ。

外部ストレージの場合、URI の最後 / (%2F) 以降がファイル名 = ディスプレイ名となっているが、GoogleDrive の場合は異った。
URI が REST のような末尾 /acc=1;doc=1 になっている。
DocumentFile.getName すればファイル名が得られる。
ただ、得られる名前は DocumentsProvider の実装依存でもあるので、必ずファイル名とも限らない。


吉里吉里2/Z のファイルパスは、ディレクトリのパス + / + ファイル名を期待した作りになっている。
TVPGetPlacedPath 内でファイル名をテーブルから検索して、見付かった場合に パス + ファイル名 で強制的に結合される。
これらを踏まえて Android版で SAF を使用したファイルアクセスをどうするか。

1. iTVPStorageMedia を拡張して(一部プラグインに影響する破壊的変更)、パスとファイル名の結合をメディアストレージに依存する実装に変える。
2. GetLocallyAccessibleName で、最後の / 以降はディスプレイ名が使われているという前提で、/ でパスとファイル名を分離し、期待するパスを生成する。

簡単に思いつくのはこの2つ。
1 は一部互換性を破壊しつつも、SAF のようなシステムを考慮したスマートな仕様に思える。ただし、このようなシステムがどの程度あるのか疑問で、この特殊な例のためだけに変更するのが良いかどうか。
2 はやや泥臭いが互換性は破壊せず、GetLocallyAccessibleName と言うメソッドも意図した動作をするように実装される。

2番で行く。

投稿者 Takenori : 22:56

2016年12月19日

吉里吉里Z 開発 :: Android 版はファイル名の大文字小文字を区別するように

吉里吉里2/Z はファイル名の大文字小文字を区別しない(ケースインセンシティブ)。
Android 版も同じような動作を考えていたが、実装上解決できないので区別することにする(ケースセンシティブ)。
問題は、同名の大文字小文字だけが違うフォルダ or ファイルが存在した場合、識別できないから。
Windows/MacOSXはファイル名の大文字小文字は区別しないが、Android/Linuxは区別する。
このOSの違いによって上述の仕様差異が生じる。

ただし、XP3アーカイブに入れたファイルはどちらの環境でも大文字小文字を区別しない。
これは OS の影響を受けないため。

上記、Windowsは区別しないと書いたが、厳密にいえばプラグインによっては区別するものが存在する可能性はある。
吉里吉里は、プラグインによって任意のストレージを追加できるが、大文字小文字を区別するかはそのストレージを追加する処理に依存する。

仕様を整理する
1. XP3アーカイブ内のファイルは環境にかかわらず大文字小文字を区別しない。
2. Windows 版はローカルファイル/フォルダの大文字小文字を区別しない。
3. Android 版はローカル/Asset/SAFのファイル/フォルダの大文字小文字を区別する。


以上のようになる。
リリース時にXP3にすべてのファイルを入れる形であれば違いは表面化しない。
確か KAG3 はファイル名の大文字小文字は気にしない実装だったので、区別する環境で動かすとエラーが出る。
ファイル/フォルダ名はすべて小文字にするなどの対応を取っていれば問題はあまり表面化しない(元々あるディレクトリにアクセスする場合は意識する必要がある)。


もし Android でこの制限を回避する妙案があったら教えてください。

投稿者 Takenori : 23:20