2019年08月19日

Mac OS 10.14 で wxWidgets 開発(その2)

 「Mac OS 10.14 で wxWidgets 開発(その1)」のつづき。

4. wxWidgets のビルド (Mac OS 10.6 対応)

 wxWidgets は 3.0.3 を使う。現時点での最新は 3.1.2 だけど、これには Mac OS 10.7 以上が必要なので、10.6 サポートを切らないのなら 3.0 系列が必須となる。

$ cd path/to/wxWidgets-3.0.3
$ mkdir build-osx; cd build-osx
$ ../configure --with-osx_cocoa --with-macosx-version-min=10.6 --with-macosx-sdk=/Developer/SDKs/MacOSX10.6.sdk --disable-shared --enable-monolithic | tee configure.log
$ (date 2>&1; make -j 4 2>&1; date 2>&1) | tee make.log

 本当は --prefix を指定して make install までやっておいた方がいいような気もするんだけど、今のところこれでも動いている。あまり行儀のいい使い方ではないかも。

5. wxWidgets のビルド (mingw-w64, クロスコンパイル)

 --host を指定するだけで、クロスコンパイルできる。

  #  32 bit 版
$ cd path/to/wxWidgets-3.0.3
$ mkdir build-win32; cd build-win32
$ ../configure --host=i686-w64-mingw32 --disable-shared --enable-monolithic | tee configure.log
$ (date 2>&1; make -j 4 2>&1; date 2>&1) | tee make.log
  #  64 bit 版
$ cd path/to/wxWidgets-3.0.3
$ mkdir build-win32; cd build-win32
$ ../configure --host=x86_64-w64-mingw32 --disable-shared --enable-monolithic | tee configure.log
$ (date 2>&1; make -j 4 2>&1; date 2>&1) | tee make.log

 wxWidgets のコンパイルはすごく時間がかかる、というイメージがあったんだけど、実際には5分ぐらいで終わった。クロスコンパイルだと、サンプルをビルドしないからかな。

 実はこのままだと、libwinpthread-1.dll という dll に依存する実行プログラムができてしまう。これは既知の問題で、基本的にはリンカオプションで回避するのだが、たまにどうしてもうまくいかないことがあるので、dll の名前を変えて強制的に静的リンクさせるようにする。(Cf. [MinGW] Link pthread statically [Ubuntu])

$ for i in i686 x86_64; do 
  cd /usr/local/homebrew/Cellar/mingw-w64/6.0.0_2/toolchain-${i}/${i}-w64-mingw32/lib
  for j in libpthread.dll.a libwinpthread.dll.a; do
    mv $j _${j}
  done
done
6. Xcode のプロジェクトを作る

 wxWidgets のサンプルプログラム "Life" をビルドする Xcode プロジェクトを作ってみる。

 Xcode で新規プロジェクトを作成する。"Cocoa App" を選択する。

20190819-1.png

 Storyboards, Document-Based, Core Data, Unit TEST, UI Tests は全部オフにしておく。

20190819-2.png

 Info.plist 以外のファイルは不要なので、削除してゴミ箱に入れる。

20190819-3.png

 wxWidgets の "demos" の中にある "Life" のファイルを、"wxLife" フォルダ(どこでもよいが)にコピーする。

20190819-4.png

 ビルドに必要なファイルを Xcode のファイルリストに加える。

20190819-5.png

 ビルド設定を変更する。やり方をすぐ忘れてしまうのでメモ:ファイルリストの一番上のプロジェクト名を選択して、"Build Settings" を選ぶと、その下にビルド設定が表示される。

20190819-6.png

  • Architectures : Base SDK : Mac OS X 10.6
  • Deployment : macOS Deployment Target : macOS 10.6
  • Linking : Other Linker Flags : -L$(PROJECT_DIR)/../wxWidgets-3.0.3/build-osx/lib -lwx_osx_cocoau-3.0 -lwx_osx_cocoau_gl-3.0 -lwxregexu-3.0 -lwxtiff-3.0 -lwxjpeg-3.0 -lwxpng-3.0 -lz -lpthread -liconv
    # wxWidgets-3.0.3 への相対パスが正しくなるように注意。
  • Search Paths : Header Search Paths : $(PROJECT_DIR)/../wxWidgets-3.0.3/include $(PROJECT_DIR)/../wxWidgets-3.0.3/build-osx/lib/wx/include/osx_cocoa-unicode-static-3.0
  • Search Paths : Library Search Paths : $(PROJECT_DIR)/../wxWidgets-3.0.3/build-osx/lib
  • Apple Clang - Custom Compiler Flags : Other C Flags : -D__WXMAC__ -D__WXOSX__ -D__WXOSX_COCOA__
  • Apple Clang - Language - C++ : C++ Language Dialect : GNU++98 [-std=gnu++98]
  • Apple Clang - Language - C++ : C++ Standard Library : libstdc++ (GNU C++ standard)
    # C++11, libc++ のままだと Mac OS 10.6 SDK ではビルドできない

 "Build Phase" を選び、"Link Binary with Libraries" を開いて、次の5つのフレームワークを加える:Carbon, Cocoa, AudioToolbox, OpenGL, IOKit

20190819-7.png

 これでビルドに成功する。おー動くやん。

20190819-8.png

 実はこのサンプルには Mac 上で画面が正しく更新されない不具合があります。life.cppLifeCanvas::DrawChanged() の終了直前に下の3行を追加する。

#if __WXMAC__
       Refresh();
#endif

 "Puffer Train" を 1500 世代まで進めたところ。

20190819-9.png

7. MinGW 用の Makefile を作る

 makefile.unx をコピーして makefile.mingw とし、次のように修正。


CXX = $(shell wx-config --cxx)

PROGRAM = life.exe       # .exe を追加

OBJECTS = life.o dialogs.o game.o reader.o liferc.o    # $(PROGRAM).o を life.o とする。liferc.o を追加。

# implementation

.SUFFIXES:	.o .cpp

.cpp.o :
	$(CXX) -c `wx-config --cxxflags` -o $@ $<

all:    $(PROGRAM)

$(PROGRAM):	$(OBJECTS)
	$(CXX) -o $(PROGRAM) -static-libgcc -static-libstdc++ $(OBJECTS) `wx-config --libs`
  #  -static-libgcc -static-libstdc++ を追加

#  次の2行を追加
liferc.o : life.rc
	`wx-config --rescomp` -o $@ $<

clean: 
	rm -f *.o $(PROGRAM)

 wxLife のディレクトリに移動する。次のコマンドで life.exe がビルドできる。PATH を指定しているのは、wx-config を呼び出すため。これは、32 bit 版/64 bit 版の wxWidgets をビルドしたディレクトリを指定する。

$ PATH="../../wxWidgets-3.0.0/build-win32:$PATH" make -f makefile.mingw  # 32 bit
$ PATH="../../wxWidgets-3.0.0/build-win:$PATH" make -f makefile.mingw  # 64 bit

 ちゃんと動いております。

20190819-10.png

8. MinGW 上のビルドを Xcode から実行する

 上の Makefile を Xcode から呼び出すには、「ターゲット」を作ればよい。これもすぐ忘れてしまうのでメモ。ターゲットは下の四角のところに表示されているので、ここのポップアップメニューを開いて、"Add Target..." を選ぶ。

20190819-11.png

 ターゲットのタイプは、"Cross Platform" で "External Build System" を選ぶ。

20190819-12.png

 ターゲットの名前を wxLife_win32 などとする。

20190819-13.png

 "Info" で作業ディレクトリとコマンドラインを指定する。コマンドは /usr/bin/env として、PATH を設定してから /usr/bin/make を呼び出すようにする。

  • Build Tool: /usr/bin/env
  • Arguments: PATH=$(PROJECT_DIR)/../wxWidgets-3.0.3/build-win32 /usr/bin/make \-f makefile.mingw
  • Directory: $(PROJECT_DIR)/wxLife

20190819-14.png

 command-B (Build) で 32 bit 版がビルドされます。64 bit 版をビルドするには、もう1つ別のターゲットを作って、PATH の設定を build-win にすればよい。

タグ:Mac wxWidgets
posted by toshinagata at 00:35| 日記

2019年08月18日

Mac OS 10.14 で wxWidgets 開発(その1)

 新 MacBookPro で、wxWidgets の開発環境を立ち上げた。目標は、「Mac OS 10.6 上で実行できる 64 bit アプリを作ること」および、「Windows の 32 bit, 64 bit アプリを両方作ること」。

1. 開発環境の整備

 先日の Alchemusica のビルドの時に済ませていたのだが、ここに書いておく。以前に書いた「wxWidgets のクロスコンパイル (PowerPC 編)」と同じく、XcodeLegacy を使う。

(1) Xcode 10 と Command Line Tool for Xcode 10 をインストールしておく。Mac App Store 経由ではなく、Apple Developer サイト https://developer.apple.com/ の Download → More で探してダウンロードするのがよい。(昔の Xcode をダウンロードするために、いずれこのサイトが必要になる)。ちなみに、Firefox (68.0.1) ではこのページの表示がおかしかった。Safari が必要かもしれない。

20190817-1.png

(2) XcodeLegacy をダウンロード・展開する。

(3) (1) のサイトから Xcode 3.2.6 のインストーラをダウンロードして、XcodeLegacy のフォルダに入れておく。

(4) 次のように実行。今回は、10.6 の SDK だけインストールしておく。

$ cd path/to/xcodelegacy-master
$ chmod +x Xcodelegacy.sh
$ ./Xcodelegacy.sh -osx106 buildpackages
$ sudo ./Xcodelegacy.sh -osx106 install
$ sudo mkdir /Developer
$ sudo ln -sf '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs' /Developer/SDKs

2. mingw-w64 クロスコンパイラ

 Windows アプリを Mac 上でビルドするため、mingw-w64 のクロスコンパイラを導入する。以前に「wxWidgets のクロスコンパイル」で書いた時は、Darwin 用にビルドされたバイナリを使った。しかし、これはかなり古くて、gcc が 4.9 である。最新の mingw-w64 では gcc8 なので、さすがに古すぎるでしょう。

 自前でクロスコンパイラをビルドしてみよう、としばらく頑張ったが、あちこちでつまづいた。結局あきらめて、Homebrew を使うことにした。mingw-w64 のダウンロードページでは MacPorts 版へのリンクが張ってあるが、Homebrew 版もある。ただし、普通に Homebrew を使うと、/usr/local がとっ散らかるので、イレギュラーだが /usr/local/homebrew にインストールすることにした。

ここは考え方が分かれるところ。Homebrew 公式では、「/usr/local にインストールする方がトラブルは少ないよ」と書かれていて、それは確かに一理ある。でも、自分としては、サードパーディのパッケージマネージャーに /usr/local をいじられるのにどうしても抵抗を感じる。結果として mingw-w64 は無事動いたので、当面はこの運用でいくつもり。

$ cd /usr/local
$ sudo mkdir homebrew
$ sudo chown $USER:staff homebrew
$ curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C homebrew
$ export PATH=/usr/local/homebrew/bin:$PATH   # .bash_profile にも入れておく。
$ export HOMEBREW_CACHE=/usr/local/homebrew/cache   #  同上
$ brew install mingw-w64
...

 標準の /usr/local へのインストールだと、バイナリをロードしてくるだけなのだが、非標準のインストールだと、一からビルドするので時間がかかる。それでも1時間かからずに終わった。

 ついでに、ターミナルから Homebrew の使用/不使用を簡単に切り替えられるように、次の関数定義を .bash_profile に入れておいた。

enable_brew() { disable_brew(); export PATH=/usr/local/homebrew/bin:$PATH; }
disable_brew() { export PATH=`echo -n $PATH | awk -v RS=: -v ORS=: '$0 != "/usr/local/homebrew/bin"' | sed 's/:$//'`; }

3. 10.6 対応の gfortran のビルド

 普通の人は関係ないと思うけど、私には gfortran が必要なのです。これも、10.6 で動くバイナリが作れないといけない。ライブラリの依存とかが面倒なことになるので、-isysroot /Developer/SDKs/MacOSX10.6.sdk の条件で gfortran 自体をビルドすることにした。

 以前に gcc 4.7 を fortran 付きでビルドした。今回は、なるべく新しいものを、と思って、まずは gcc 9.1 をビルドしてみたのだが、途中でこんなエラーが出て沈没した。

../../../../gcc-9.1.0/libsanitizer/sanitizer_common/sanitizer_mac.cc: In function ‘bool __sanitizer::GetRandom(void*, __sanitizer::uptr, bool)’:
../../../../gcc-9.1.0/libsanitizer/sanitizer_common/sanitizer_mac.cc:1081:3: error: ‘arc4random_buf’ was not declared in this scope; did you mean ‘arc4random_stir’?
 1081 |   arc4random_buf(buffer, length);
      |   ^~~~~~~~~~~~~~
      |   arc4random_stir

 調べてみると、arc4random_buf() は Mac OS 10.7 以降でしか使えない。そこで、バージョンを1つ下げて gcc 8.3 に切り替えた。次のようにして、無事ビルドできた。

$ sudo mkdir /usr/local/gcc8
$ cd /Developer/SDKs/MacOSX10.6.sdk/usr/local; ln -s /usr/local/gcc8 gcc8
$ cd path/to/gcc8.3
  #  gcc8.3 ディレクトリの中に gmp-5.0.5, mpfr-3.1.2, mpc-1.1.0,
  #  isl-0.21, gcc-8.3.0 を展開してある
$ cd gmp-5.0.5
$ mkdir build; cd build
$ ../configure --prefix=/usr/local/gcc8 --disable-shared --enable-static --with-sysroot=/Developer/SDKs/MacOSX10.6.sdk CFLAGS=-fPIC CXXFLAGS=-fPIC | tee configure.log
$ make -j 2 2>&1 | tee make.log
$ make check | tee makecheck.log
$ sudo make install
$ cd ../../mpfr-3.1.2
$ mkdir build; cd build
$ ../configure --prefix=/usr/local/gcc8 --disable-shared --enable-static --disable-thread-safe --with-sysroot=/Developer/SDKs/MacOSX10.6.sdk CFLAGS=-fPIC CXXFLAGS=-fPIC --with-gmp=/usr/local/gcc8 | tee configure.log
  #  --disable-thread-safe 重要!
$ make -j 2 2>&1 | tee make.log
$ make check | tee makecheck.log
$ sudo make install
$ cd ../../mpc-1.1.0
  #  mpc-0.9 だと --with-sysroot がないので面倒
$ mkdir build; cd build
$ ../configure --prefix=/usr/local/gcc8 --disable-shared --enable-static  --with-sysroot=/Developer/SDKs/MacOSX10.6.sdk CFLAGS=-fPIC CXXFLAGS=-fPIC --with-gmp=/usr/local/gcc8 --with-mpfr=/usr/local/gcc8 | tee configure.log 
$ make -j 2 2>&1 | tee make.log
$ make check | tee makecheck.log
$ sudo make install
$ cd ../../isl-0.21
$ mkdir build; cd build
$ ../configure --prefix=/usr/local/gcc8 --disable-shared --enable-static --with-sysroot=/Developer/SDKs/MacOSX10.6.sdk --with-pic --with-gmp-prefix=/usr/local/gcc8 | tee configure.log 
$ make -j 2 2>&1 | tee make.log
$ make check | tee makecheck.log
$ sudo make install
$ cd ../..
$ mkdir build-gcc; cd build-gcc
$ ../gcc-8.3.0/configure --prefix=/usr/local/gcc8 --with-sysroot=/Developer/SDKs/MacOSX10.6.sdk --with-build-sysroot=/Developer/SDKs/MacOSX10.6.sdk --with-gmp=/usr/local/gcc8 --with-mpfr=/usr/local/gcc8 --with-mpc=/usr/local/gcc8 --disable-nls --disable-tls --disable-multilib --enable-languages=c,c++,fortran --with-pic | tee configure.log
  #  --disable-tls 重要!
$ (date 2>&1; make -j 4 2>&1; date 2>&1) | tee make.log
$ sudo make install

 gfortran のスタティックリンクのため、/usr/local/gcc8/lib/gfortran.spec を修正。

#*lib: -lquadmath -lm %(libgcc) %(liborig)
*lib: -lm %(libgcc) %(liborig)
  #  lquadmath を無条件にリンクするのをやめる

 リンク時には、-static-libgfortran -static-libgcc /usr/local/gcc8/lib/libquadmath.a を指定する。これで、libgfortran.dylib, libquadmath.dylib に依存しないバイナリができる。

 長くなったので、いったん切ります。次は Xcode プロジェクトの設定など。

タグ:Mac wxWidgets
posted by toshinagata at 00:58| 日記

2019年08月13日

CoreAudio: Sound Canvas VA の音(だけ)にノイズが乗る

 新 MacBookPro でいろいろなアプリの動作を検証していて、あれっと思った。Alchemusica で Sound Canvas VA を鳴らして、イヤホンで聞くと、ノイズが乗る。内蔵音源とか LinuxSampler とか、他の音源ではそういうことはない。録音済みの aiff を再生しても問題ない。イヤホンを外して、スピーカーで鳴らすと、正常に鳴っている。「Sound Canvas VA +イヤホン」の組み合わせだけがおかしい。

 いろいろ調査していて、一つ気づいたのがこれ。イヤホン(ヘッドホン端子)への出力は、デフォルトで 48000 Hz になっている。

20190813-1.png

 これを 44100 Hz に変えると、ノイズはなくなった。CoreAudio の内部は基本的に 44100 Hz で動いているので、48000 Hz の場合はどこかでサンプリングレートのミスマッチを起こしている可能性が高い。それにしても、どうして Sound Canvas VA だけ?

 調査にとりかかった。Alchemusica のオーディオ出力は、Audio Settings の Bus 1 〜 Bus 40 の出力をミキサーで混ぜて、それを Output デバイスに送る仕組みになっている。CoreAudio の言葉で言えば、ミキサーは kAudioUnitType_Mixer, kAudioUnitSubType_StereoMixer、出力デバイスは kAudioUnitType_Output, kAudioUnitSubType_DefaultOutput で指定される AudioUnit である。出力デバイスを変更すると、AudioUnitkAudioOutputUnitProperty_CurrentDevice が変更される。この2つの間は、コールバック関数で接続されている。このコールバック関数は、ミキサーからデータを取得して出力デバイスに流すが、オーディオ録音を行う時には同時にファイルへの書き込みも行っている。

 最初に考えたのは、ミキサーの出口が 44100 Hz, 出力デバイスの入り口が 48000 Hz だから、コンバータをかませばいいのでは、という案。しかし、やってみるとこれはうまくいかなかった。ミキサーの先にコンバータをつないで 48000 Hz で出すと、コールバック関数でデータをもらうときに AudioUnitRender() 関数がエラーを吐く。現状でも「Sound Canvas VA 以外」ではちゃんと動いているのだから、これでは一歩後退だ。

 コールバック関数は次のようになっている。これがどう呼ばれるかをもう少し調べてみた。

static OSStatus
sMDAudioRecordProc(void* inRefCon, AudioUnitRenderActionFlags* ioActionFlags, 
  const AudioTimeStamp* inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, 
  AudioBufferList* ioData)
{
    OSStatus err = noErr;
    /*  Render into audio buffer  */
    err = AudioUnitRender(gAudio->mixerUnit, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData);
    ...
}

 すると、出力デバイスが 44100 Hz だと inNumberFrames512 であるのに対して、48000 Hz だと 471470 で呼ばれることに気づいた。471/44100 ~ 512/48000 ~ 0.0107 だから、471 または 470 フレームのデータを受け取って、それを内部で 512 フレームに割り直して出力すれば、ちょうど 48000 Hz になる。つまり、サンプリングレートの変換は、出力デバイスの方が自分で面倒を見ていることになる。

 この inNumberFrames は、AudioUnitRender() でミキサーに要求するデータの数でもある。ミキサーはその先で、入力側につながっている AudioUnit に同じ数のデータを要求しているはず。ということは、Sound Canvas VA は、データ数が 512 なら正常にデータを送るが、471 や 470 の場合は正しくデータを送っていないのではないか。要求されるデータ数が2の累乗であることを暗黙に仮定する、というのはいかにもありそうなコーディングスタイルだし、わざわざ 48000 Hz のデバイスをつないでテストしない限り問題なく動作してしまう。

 それじゃ、こちらで回避する方法はあるのか? データの要求数を「キリのよい数」にしておいて、それをバッファに保存しておき、必要な数だけ送り出せばいい。幸い、入力デバイスからのオーディオ入力の実装のためにリングバッファを作ってあったので、このコードを流用することにした。


/* エラー処理は省略してあります */
static OSStatus
sMDAudioRecordProc(void* inRefCon, AudioUnitRenderActionFlags* ioActionFlags, 
  const AudioTimeStamp* inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, 
  AudioBufferList* ioData)
{
    OSStatus err = noErr;
    MDAudioIOStreamInfo *info = (MDAudioIOStreamInfo *)inRefCon;
    MDSampleTime startTime, endTime, renderTime;

    /*  Sample time range to handle during this callback  */
    startTime = inTimeStamp->mSampleTime;
    endTime = startTime + inNumberFrames;
    if (info->firstInputTime < 0) {
        info->firstInputTime = inTimeStamp->mSampleTime;
    }

    /*  Fill the ring buffer until enough data is present in the ring buffer  */
    while ((renderTime = MDRingBufferEndTime(info->ring)) < endTime) {
        AudioTimeStamp timeStamp = {0};
        int i, n;
        timeStamp.mSampleTime = renderTime;
        timeStamp.mRateScalar = inTimeStamp->mRateScalar;
        timeStamp.mFlags = kAudioTimeStampSampleTimeValid | kAudioTimeStampRateScalarValid;
        n = info->bufferSizeFrames;
        for (i = 0; i < info->bufferList->mNumberBuffers; i++)
            info->bufferList->mBuffers[i].mDataByteSize = n * info->ring->bytesPerFrame;
        err = AudioUnitRender(gAudio->mixerUnit, ioActionFlags, inTimeStamp, inBusNumber, n, info->bufferList);
        /*  Write to ring buffer  */
        err = MDRingBufferStore(info->ring, info->bufferList, n, renderTime);
    }
    
    /*  Fill ioData from the ring buffer  */
    err = MDRingBufferFetch(info->ring, ioData, inNumberFrames, startTime, false);
}

 だいぶ苦労したけど、動くようになりました。CoreAudio は難しいねえ。

タグ:Mac Alchemusica
posted by toshinagata at 16:09| 日記
email.png
Powered by さくらのブログ