とりあえずの謎日記 あまり技術論は期待しないでください。いろいろ知ってるつもりだけど この手のプログラムは、SL-ZaurusというかQtを少し触ったくらい。 それなりに経験は生かせているようですけど。 2004.5.5 武田殿@EmuZ-2500より啓示を受ける。 といってもTK-80シミュレータを見た時から作りたかったものだったのだけれど。 本当は別のものを作りたくて、でもWin32は全くの未経験で、それで手軽な開発環境を 構築したく、でもマイクロソフトにはOS以外のカネを払いたくなく、無料で手に入る 環境はないかと探していたのですよ。 最初はMinGW。あちこち調べると悪くなさそうではあるが、どーも不安定らしい。 次にBorland C++。以前無料化されたということでCマガだけは買ってあったんだけど コマンドラインってのがな〜ってどうせ使い始めればそんなことはものともしない 根性はあるけど、リソースエディタがないってのはどうなんだろ。それにまるくんの mz700winでもあったけど、VC++のプロジェクトがそのままは使えんとかなんとか、 よくわからんが避けたほうがいいのかな? そういう意味ではVC++ Toolkit 2003がいいのかも。最近最適化されたコンパイラが 配布されているようだし、より一般的な環境に最も近い無償環境ということになる ような気がする。でもやっぱりコマンドラインか。 最後がOpen Watcom、あのWatcomコンパイラのフリー版。IDEもリソースエディタも あるということで、とりあえずダウンロード…いや、寄付はかんべんして下さい… インストール。 「猫でもわかるプログラミング」とか、「Win32入門」とかサンプルいっぱいで 解説してくれているサイトがいくつかあるので、それを参考にちょっとテスト。 … … … コンパイルできないんですけど。エラーが出てて、キャストが必要みたいなのに してないぞ、というメッセージがあるんだけど。その行を飛ばすとウィンドウが出ない し。うーん。 と言いつつ一晩明けてもう少し解説を読んでみると、 myProg.hbrBackground = GetStockObject(WHITE_BRUSH); ではなく myProg.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); が正解らしい。自信満々に書いてるから、間違いないと思うじゃない。いかんぞ、 猫わか。 前途は多難なれどもOpen Watcomでなんとかなる、と確信したところでたごさん とこの掲示板にカキコすると、さらに一晩明けて武田さんの「EmuZ-40K作るの?」 (大意)というコメントが。ああ、忘れておりましたよそんなこと。 でも作れそうですね。一発挑戦しますか!! 基本方針は ・出来上がりイメージはTK-80シミュレータ。つまり基板の絵があって、LEDと キーボードのみが動き、ワンボードマイコンを表現する。 ・どうせROMの内容を吸い出せないので、よくあるエミュレータとしての実装は せず、外見の動作を再現する。 ・オプションのうちMZ-40K1(センサーキット)は再現が難しいので絵だけ、MZ-40K2 (音楽用鍵盤)は再現する。 ・その他、Windowsならではの多少便利な機能をつける。 ということでいいでしょう。 で、まずは絵。 幸いにしてマニュアルに基板図なるものが掲載されているので、それをスキャン。 これで少なくともパターンとシルク(基板上の白い文字とかね)はよくわかります。 パターンは拡大してみるとスクリーントーン風の点々の集合であるうえにモアレが 発生してそのままでは使えない状態。これはなぞって塗っていくしかないですね。 シルクは黒い文字になっているところを反転して重ねればいけるはず。 2004.5.16 絵が完成。 まさに自画自賛ですが、すごいリアルに描けましたよ。 結局、シルクは反転して黒くなったところを透明化する方法がわからず普通に文字を 書く方法に切り替え。 パターンは一時見えなくなるところも律儀に描いていこうと思ったけれど、本当に 見えないところを描いても空しいのである程度部品を描くのを優先することに。 部品を描く最中、ぼかしの使い方とかがわかってきて、俄然出来栄えがリアルに。 まぁ金属光沢とか真鍮とか難しいんでそれっぽく表現するしか腕がないんですけど、 それを差し引いても素晴らしい出来栄え。 2300ドット四方以上の絵でありながら手法はドット絵だったりして、見えるところ だけでもすごいリアルなんですが…縮小するとわからないなぁ。リード線の光沢とか、 抵抗のカラーコードとか、律儀に描いたんですけどねぇ(^_^;)。 一応、パターンはスキャナ取り込みのものを上からなぞり、表面のメーカーと型番の ロゴはやはり本物を取り込んだ後ドット単位で修正しましたが、その他は全部手書き です。まぁコピー&ペーストとかは多用してますけどね。 レイヤーを合成してしまうのはもったいないので、そのまま保管です。 2004.5.17 今日からコーディングです。 キーの大きさの関係で縮小率を変える可能性が高いんですけど、とりあえずそれまでに ベースとなるMZ-40Kの絵をウィンドウに表示しておかないといけないんで。 とりあえず猫わかからBMP表示のサンプルをいただいて、リソースなどを書き換え自分 用に修正。サンプルでは文字表示とかウィンドウ内での縮小コピーとかがありましたが ざっくり取り払って、まずは成功。 でも表示したい絵に対してウィンドウが狭い。CreateWindow()でサイズをお任せにして いるせいだろうということで、絵のサイズ556×600ドットをパラメータに直書き。 しかしそれでも狭い。リサイズは効くので調節してみると、どうもウィンドウの枠が 556×600ドットということになっているらしい。なるほど。 目で測りながらドンピシャの位置を探すこともできるんですけど、クラシックスタイル とか、もっと枠が太い設定の人もいるでしょうから、システムかそういう情報をもらう 必要がありそうです。 それでいろいろ調べてみると、GetSystemMetrics()という関数でそういうスタイルとか の情報を得られるらしいということで、いろいろ試して GetSystemMetrics(SM_CXFRAME) 左右のウィンドウ枠の太さ GetSystemMetrics(SM_CYFRAME) 上下のウィンドウ枠の太さ GetSystemMetrics(SM_CYCAPTION) タイトルの太さ でいけることがわかりました。これを画像サイズに適宜足し合わせればスタイルに左右 されず意図どおりに表示できるということですね。 一旦成功したあと、リサイズ不可・最大化不可の設定にするためCreateWindow()のパラ メータに WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX, を加えたところ空白が余ってしまったので、もう一回探して GetSystemMetrics(SM_CXFIXEDFRAME) 固定ウィンドウの左右の枠の太さ GetSystemMetrics(SM_CYFIXEDFRAME) 固定ウィンドウの上下の枠の太さ に変更。 しかし、上のウィンドウ枠ってタイトルバーに含まれるのかと思ったら、別扱いとはね。 2004.5.19 MZ-40Kは何もしてない時は時計を表示しているので、まずは時計機能の実現。 つまりLEDを表示させるわけですが、最初はセグメントごとに必要なものだけ描くべき かと思っていたところ、TK-80シミュレータのソースというかリソースを見てみると 全てのパターンについて一桁単位でまとめたものを作ってありました。ドットも含める と8セグメントあるわけで、組み合わせは256種類。 MZ-40Kを実機で調べてみると、ビット単位でセグメントを制御できる機構がないので 表示できるのは16進数の0からFと秒表示のドットがある0から9、それとSTOP押しっぱなし で無表示になる計27種類しかありません。これだけのリソースを用意しておけばいい ですね。 コントラスト差を大きくするために原画のLEDの色を暗くし、縮小して各文字のパターン を描きます。ドットが少なくなる分荒い絵になるけど、実寸で見ると悪くない感じ。 いや、ちょっと小さくないかな? で、できあがったやつをリソーススクリプトに加えて、まずはリセット直後オールゼロ 表示であることから全桁ゼロの表示。あらかじめ表示したい数値と桁とドット付きか 否かをパラメータに持つサブルーチンを作っておいて、全体表示直後に追加して表示 させると、なかなかいい感じ。 次に時計機能。システムで持っている時刻をただ表示するのではイマイチなので、時刻 取得は起動時のみとし、あとは自前でカウントすることに。タイマーで100msごとに メッセージをもらい、10カウントで秒をインクリメント、以下同様。100msごとに動作 させるのは、できるだけ秒のドット点滅の誤差をなくするため。それとオルガン機能で 使用する予定。 点滅に成功したものの、LEDが少しチラつきます。予想はしてたものの、結構あっさり この現象が出ましたね。グラフィカルなプログラムではウィンドウの重なりとかで どうしても再描画しないといけなくなるわけですが、それが大きいとか細かいくせに わずかな領域でも全体を再描画しないといけなくしてしまうと、どこかがチラつく わけです。これはザウルスでも経験済み。対処法は、必要最小限だけ書き換えるように すること。 APIを探してみると書き換えたい領域を指定してメッセージを出させるものがあった ので(InvalidateRect())、とりあえず再描画領域を絞ってみると全体の再描画しか しなくてもチラつきはかなり抑えられます。でもたまにチラつく。 そこで描画部分でも再描画に指定された領域の座標を調べて必要がなければ全体描画 までしない、とするとチラつきはなくなったみたいです。 で、あとはメニュー。とりあえずFile→ExitとHelp→Aboutのみ。なくてもいいような アイテムですけど、Aboutがないと誰が作ったかわからないので、MessageBoxで作る ダイアログを取り急ぎ実装してなんとか完成。 まだまだ実装する要素は多いですが、まずは0.1版をリリース。これでウチ以外の マシンにもMZ-40Kが再現されるんですね、とりあえず見た目だけ。 2004.5.22 次にキー入力。 まずは背景用BMPからキーボード部分を抜き出し、キーを離した状態の絵と押した時の 絵を作成。最初はTK-80シミュレータ同様一直線にキーを並べようと思ったものの、 原画では等間隔・等幅なキーが縮小で誤差を生じているため、配列にキーの位置と大きさ を収めてリソースから切り出すことに変更。 キーが押された絵は、TK-80シミュレータのように斜めに移動するものではなく、押される ことで周囲より凹む→陰になる、という意味から凸部の下の部分を暗くすることに。 ついでに視点からやや遠ざかるからちょっとだけキーの文字を縮小。 で、キーはオーナードローボタンというやつで作成することに。for文でCreateWindow() し(ボタンというのは属性で、実は子ウィンドウ。これが後で波乱を呼ぶ)、それに従って WM_DRAWITEMというメッセージが飛んでくるのでボタンを描画する、という按配で 作ったものの、まるっきり作成されず。どうしてWM_DRAWITEMにあるコードが実行されない んだろう??と思案して、まさかと思って作成時に飛んでくるWM_MEASUREITEMを捕まえ ようとしたら無反応。え、まさかCreateWindow()が失敗してるのかとよく見たらウィンドウ クラスを"BUTTON"にしてませんでした(ユーザーで定義した任意の名前かと思ってた)。 これで作成はされるものの、まだ描画されない。いや、描画ルーチンは押すか押してない かでリソースを選択し指定されたキーをその位置に描くように共通化したものにして いたんだけれど、そこにキーの番号が伝わってないらしい。 CreateWindow()でhMenuというHMENU型の引数があって、これがボタンの時はそのIDになる らしいけど、これが真にボタンの識別に使えるのかしらん?と疑問だったのが、いろいろ 調べてみるとそれで合ってるらしいことがわかりました。WM_DRAWITEMメッセージが 来るときには、lParamにそのIDが入っているということで、それを描画ルーチンに 渡して…あれ、まだ出ない。 描画関係のこれまで描いてきたルーチンなどを見て、デバイスコンテキストの説明なんか も見ているうちに、ようやく理解。CreateCompatibleDC()で作るデバイスコンテキスト は、大きさとかはそのボタンそのものになるけど、新たに作るのでそれだけで独立した 座標系になる、つまりその左上の座標は全体のボタンの位置ではなく原点になるわけか。 ということで座標を変えてようやくボタンが現れました。まぁ背景と同じなんですけど。 WM_DRAWITEMの処理でボタンの押下状態を判別して、押した時に絵が変わることも確認。 これで、一応押した感じが出てるかな? キーが動くようになったので、次にその動作の実装。最初はSTOPキー。 ワンボードマイコンのSTOPキーはたいていリセットにつながっていて、押している間 リセット状態が続きます。MZ-40Kでも同様で、押している間LEDの表示が消え、離すと 時計モードになります。 まずは、動作モードを保持する変数を用意。0を無表示、1を時計モードとして、STOP キーを押している間は0、離したら1に変化させます。変化と同時にInvalidateRect() で更新を指示、WM_PAINTメッセージ処理部に無表示を加えます。これはすんなりOK。 次はCLEAR。押すと表示が0000となります。もちろん時計表示ではありません。モード を2として、表示用メモリを4つ用意、CLEARの押下時の処理として0000に初期化のあと モードを設定、InvalidateRect()します。離した時の処理はなし。当時のマイコンシステム は、押した時に変化があって離したときまで変化がない今のGUIとは違うのです。あ、 もちろん実機にて確認済み。 さらに、各数字の入力時の処理も記述。時計モードの時はCLEARとほぼ同じ動作で、 右端に押した数値が入ります。ですのでCLEARの処理の変形で対処。 よし、これでメモリはないけどキー入力にほぼ対応できました。 …。 …。 …。 あれ?ちょっと変。 起動直後は問題ないけど、一度STOPで時計モードに戻してから数値キーとかを押しても 一度では効かない。2回目でうまくいくみたいで、でもずっと入力がそんな調子ではない みたい。内部処理の競合みたいなものを想像したけど、2度押せばうまく動くというのは あまりにもロジカルなので、やはりソースを疑うべきか。 いろいろやって、デバッグ用にメッセージウィンドウを出して初めてSTOPキーが離れた のを複数回受け取っているらしいことを確認。STOPキーの役割を別のキーにまわしても 同じで、どうも全てのキーが離れた時の処理をするよう要求されているみたい。 (翌日わかったことだけど、ここ、つまりWM_DRAWITEMで((LPDRAWITEMSTRUCT)(lParam))->itemState の値を見て押されているかどうかを判別するようにしていたのだけれど、値は押されて いるというODS_SELECTEDだけでなく他にもあるわけで、そのイベントでWM_DRAWITEMが 飛んできていたらしい。なにせelseでそれ以外はみんな「離した」ことになっていたから。 で、元の方式に戻そうとしたけどよく考えたら押していることはわかっても離したことが 正確にはわからんので、やっぱりこの後の処置が正解みたい) これはやはりボタンらしく「押された」「離された」という判別をせねばならんという ことでいろいろ調べたが、ボタンはそれ自体は入力されたということは知れても「押された」 かどうかはわからんらしい。WM_COMMANDでボタンのIDを判別するのでは目的が果たせません。 やむなくWM_LBUTTONDOWNとWM_LBUTTONUPでマウスの動きを見て、その時の座標でどのキー が押されているか判断しようとSTOPキーの分だけを書いたところ、なんと動かない。 あれ…?と思いながらいじっていると、なんとキー以外のところなら反応することが判明。 ああ、なんかオーナードローボタンの解説で子ウィンドウを作るとかなんとか書いてあった 意味がようやくわかりましたよ。このマウスの動きはボタンそのものに伝わって、親で ある本体ではわからんのですね。 じゃ、実際にメッセージを受け取るヒトは誰?どこで定義するの? と思ってたら「サブクラス化」でメッセージを横取りできることが判明。なるほど、必要 なメッセージだけ処理して、他にもあるかもしれないメッセージはOSにやらせるわけです ね。 ということでサブクラス化。最初はCreateWindow()をループで処理していたのを、その プロシージャのプロトタイプ宣言とかもろもろで配列の形式をとる関数が使えなさそう だったので、あきらめて普通の宣言を22個並べました。 動かしてみると、マウスのクリックはキーに届いているようですがボタンの絵に変化が ない。どうも横取りしたメッセージは親がWM_DRAWITEMを出すときにも使っているみたい なので、「横取り」ではなく「覗き見」のようにして、つまりどんなメッセージも親に 渡すということで解決しました。 でもLEDの変化が変で、時計モードでないのに時計表示だとか、STOP押しても消えない とか。これはもしかしたらウィンドウハンドルが親のでないといかんのかと思いそれを グローバルで定義したところうまく動作。 2回押さないと動かないなんてこともなく、これでOKですね。 キー入力ができたので、あとはメモリ。512アドレスあるのでそれだけの変数を用意。 ADRキーが押されたらアドレス操作用の変数に取り込み、表示は0000。 WRITEが押されたら4桁のデータをメモリに投入、READならメモリから表示メモリに 取り出して表示。 簡単にできた、と思ったらなんか表示がむちゃくちゃ。入れたはずの値が読み出せない。 計算したアドレスをメッセージウィンドウで表示させてみると、なんか巨大な数値が。 むむ?ビットシフトがうまくいかないな。イマイチなんだけど掛け算で変換したら、 合いました。 (単に括弧をつければよかったんです。演算子の優先順位の問題) しかし、それでも動かない。もしやと思ってアドレスの範囲を絞っている&演算子の ところを括弧でくくると、正常に。やれやれ、これでなんとかなったな。 2004.5.23 日が明けて、今日はサウンドに挑戦。 waveOut系のAPIを使って、品質はともかく鳴るところまでこぎつけます。 音の入っているバッファはとりあえず適当に確保、ウィンドウ生成時の初期化部分で waveOutOpen()を実行。waveOutPrepareHeader()は音階選択でキー入力部分に必要なので そちらに記述、引数に必要な構造体のほとんどの定義を初期化部分で済ませておきます。 waveOutPrepareHeader()の直後にwaveOutWrite()で発音。音関係のデータ廃棄とかは 終了処理部分に記述します。 で、コンパイル…むむっ、リンクエラー!!! なんで?!サンプルを説明しているサイトでは特に注釈もなかったのに…。 ではそこのサンプルをコンパイルしてみよう。…やはり同じエラー! もしかしてこれがOpen Watcomの限界?これまでいろんな関数を設定も気にせずどんどん 使えていただけに、いきなりこれはちょっと…。 しかたないので、GT選手権@菅生でも観てよう。 …。 …。 …。 うーん、やっぱり荒れたなぁ、菅生。 と、ここで武田さんから助け舟が。winmm.libがリンクされているかどうか? ライブラリのディレクトリには確かにそれがあります。でも他のもあるし、パスの 指定の問題とは思えない(というかツール上にも環境変数でもライブラリパスの指定 がないし、ヘルプにもない)んだけど、とりあえずリンクのスイッチ設定のところの ライブラリのところにwinmmとだけ書いてコンパイル…通るじゃない。 一応鳴りますね。ではバッファを短く音をラにして、連続で鳴るようMM_WOM_DONEを 受けてすかさずwaveOutWrite()を実行するように変更…1回しか鳴らない。 MM_WOM_DONEの受け場所をサブクラス化したボタン処理の中にしても同じ。うーん、と 困った末にMM_WOM_DONEを使っているサンプルをコンパイル、こちらはちゃんと鳴り ますね、繰り返して。 で比べてみると、waveOutOpen()で指定したコールバック先の指定が少し間違って ました。グローバルで作った親ウィンドウのハンドルを渡していたのを、プロシージャ で使える引数としてのウィンドウハンドルを指定することで繰り返すようになりました。 でも、プププププという感じで、明らかに途切れています。まぁ覚悟はしていましたが。 これを防ぐにはスレッドで高速に動かすのがいいらしいんですが、プロシージャと 違ってどうメッセージを受け取ればいいものやら…。 と思ったらまた武田さんから助け舟が。GetMessage()を使ってメッセージを拾うらしい。 そういやいろいろ探していた時にふと読んだところで、WinMain()で記述している GetMessage()のことがあったんですけど、それの応用ということなのかな。 とりあえずこれは明日以降ということで、2ndリリースをば。 2004.5.24 武田さんの助け舟をもとに、スレッドの作成。 作成自体は簡単で、CreateThread()でスレッドにしたい関数とIDを受け取りたい変数その他 を指定するだけ。スレッドにはGetMessage()でメッセージを受け取るように仕掛けておき ます。 音関係の関数では、waveOutOpen()でさっきのIDとスレッドでメッセージを受け取る旨の 指定をしておけばOK。 いざ、実行…って、1回しか鳴らないですよ。せめて連続してププププとでも鳴って くれないと先に進めない。うーん…と思ってたら、どうもGetMessage()の対象ハンドルが おかしいようで、親ウィンドウを指定するとなんか変。スレッドのIDをハンドルと説明 するところもあったのでこれを指定すると、プログラム全体の動きが変。どこまで動いて いるかメッセージボックスを入れてみると、1回は来ているようですがそれ以降は無表示。 もしかして、スレッドが終了しちゃってます? で、もう一度GetMessage()の説明をよく読んでみると、ハンドルの指定がNULLだとどの メッセージも受け取るとのこと。なんか気持ち悪いけど、それでやってみるとププププと 連続するようにはなったので、なんかこれでいいみたいですね。 でもスレッドにしたのに切れ目はどうしても発生しちゃう。気のせいかププププの速度は 速くなったようにも思えますが、連続というより断続音であることには間違いなし。 音を出すサンプルでは1秒間だったデータ長を0.1秒に縮めていたんですが、それを元に 戻しても断続するのは変わりません。ダブルバッファにするとかいろいろいじってみても わかりません。ついにあきらめムードで掲示板にカキコ。 でももう少し手がかりを…と単純に音を発するだけのプログラムがないかと探したところ、 ベクターに任意の周波数でいくつかの波形を音として発生させるプログラムを登録して いる人を発見。ソース付きということで早速ダウンロードして、中身をチェック!! おお、なんかスレッドを使ってないですよ?!プログラムを実行してみると、見事に連続 した音が出ています。いろいろ見た感じだと、自分のプログラムとの違いは ・ヘッダやデータをロックしてからwaveOutOpen()やwaveOutPrepareHeader() ・WAVEFORMATEXではなくPCMWAVEFORMAT というのがポイントみたい。 2004.5.25 で、あれから一日PCMWAVEFORMATの調査。でも、WAVEデータファイルからのデータの 抜き出しとか、そんなのしか見つからない。どうも、これはファイルに落とす際の PCMのデータ形式らしいです。たまたまWAVEFORMATEXと先頭からの構造が同じなので、 キャストだけして使っていたみたいですね。 ではロックについて調べてみます。まずは昨晩のソフトのソースを、自分の環境で コンパイル、実行。ちょっと音を止めるところのボタンの検知がうまくいってなかった のでそれを変更して、一応再現。waveOutReset()で止めてるみたいですが、かなり 大きなプチノイズが乗りますね。やっぱりいきなり止めるのはよくないんでしょうかね。 念のため、ソースのサンプリング周波数とかデータ長とかを変化させてみても、途切れ る現象は起こらないことを確認して、次の作業。 次は自分のソースの、データをロックしてwaveOutPrepareHeader()してみます。お手本は ヘッダもロックかけてるんですが、waveOutOpen()のあと開放してたりして、あまり 再生には影響なさそうなので。 ダブルバッファにしていたのをやめてロックを取り入れ、いざ実行。 …途切れるやん。これも意味ないの? ロック関係をいろいろ調べてもよくわからんし。 とお手本のソースを見ていると、なんとMM_WOM_DONEの処理では終了処理しかしていない ことが判明。昨晩眠い頭で見てたからなぁ、見落としか見間違いだったのか。 では、どうして連続して音が出るの?まさか?!とwaveのヘッダを見てみると、ULONG_MAX 回のリピートが指定されているし…。えー、そういうことですかぁ?そりゃまぁ連続音 は出るでしょうけど、強制停止するとプチノイズが心配だなぁ。 でもやってみないとわからないので、そのように変更。実行してみると…おお、連続 してますよ。キーを離すと特に不自然でもなく切れますよ。なんだ、こんな方法で 良かったんぢゃん。スレッドにコールバックがかからないようにしても影響なし。 うー、しょうもなぁー。 2004.5.27 鳴らせるメドが立ったので、本格的にオルガンの実装開始。 いちいちヘッダ作ってwaveOutPrepareHeader()して…ていうのは面倒なので、またどうせ 3オクターブしか音は出せないし、あらかじめ全部の音を作って全てに対応したヘッダを 用意しておいてからwaveOutPrepareHeader()することに。 でその用意する音ですけど、自動演奏に備えて、音程コードを引数として目的の音が 指定できるように作ることとします。マニュアルを見るとその最大値は0x3b、最小値は 0x00ですから、0x3c個の配列を準備すればいいわけですね。ただ0x1c〜0x1fとか存在 しないコードがありますので、ちょっとムダっぽいですが。 音程そのものは中音のラが440Hzということと1オクターブ上がると周波数が倍になると いうことくらいしか知りませんでしたので、ちょっとネットで探索。 なんか、ある1オクターブ分の間を12分割するものでもないそうで、基準音から何倍 すれば目的の音が得られるか、そういう値があるんだそうですね。データは周波数から 算出することにして、とりあえず3オクターブ分の周波数を算出。 …っと、よく考えたらその中音のラはキーボードのどれで、コードとしてはどれなのか わかりませんね。 マニュアルには自動演奏用に曲がいくつか楽譜付きで紹介されているのですが、それの コードが参考になりそうです。まず掲載されている「こいのぼり」の冒頭を少し入力、 演奏させてみます。 次に、鳴っていた音がキーボードのどれにあたるのかオルガンモードでチェック。どうも、 上段のミの周辺の音を使っているようです。これのコードは明らかですから、それから 考えると上段が中音、下段が低音、キーからは入力できない範囲の音が高音ということに なりますね。 あとは周波数に応じてデータを作成し、それぞれのキー入力でwaveOutPrepareHeader()→ waveOutWrite()と離したときのwaveOurReset()を設けて、とりあえずは完成。 ではその出来栄えを…あれ、以前とは違う感じで音が途切れますね。いろいろ押してみると 途切れない音と途切れる音が決まっているようです。 音によってその周期が違うことからそのまま作るとデータの最後が中途半端な波形になり、 それをループすると不都合がありそうな気がしたので適切な長さのデータにするよう 計算させていたのですが、それがうまくいっていないということでしょうか。 部分的に実数演算させていて、小数点以下のつまりは「余り」が計算をむずかしくさせて いるようです。固定長に変えても、途切れる音が違うものになるだけで現象は続いて います。 うーん、どう計算させようかな…。 2004.5.29 そういやどこかでwaveのバッファは8バイト単位で使われるとかなんとか書いてあった ような気がします。でもどうやって8の倍数にまとめればいいんだ…? …なんだ簡単ぢゃん。8の倍数でかければいいんぢゃん。800バイト(0.1秒)くらいあれば いいので、少なめで24をかけておくことにします。うん、これで完璧。 次に、自動演奏…でなくて、ちゃんとしたダイアログを作成。いや、絵を入れたくて、 真剣に絵を修正するには休日の時間が必要なので。 絵を作るということで、ダイアログのついでにアイコンも作成することに。といっても MZ-40Kのシンボルマークはアルゴ船座ではないので、ちょっと思案したあげく、マニュアル 表紙とかゲーム用シートに印刷されている少年の顔に決定。なんか輸出版には箱にも 描かれていますので、諸外国の方にも通りは良いかと。 サンプルはカーレースのシートで、右下に「マイコン博士」と少年が描いてあるのを 用いてスキャン。で、少年の顔だけ取り出して32×32ドットに縮小。 …で、どうやって.icoファイルにすればいいの?ああ、なんかOpen Watcomにイメージツール があるので、それを使うのね。縮小したものを貼り付けて、セーブしてリソーススクリプト 書き換えて…んー?なんか色が変? 24bitカラー状態で貼り付けて、でも属性は16色だからパレットが変わっちゃってるみたい ですな。どうすれば? 調べてみると、WindowsXP用のアイコンの作り方として48,32,16ドット四方のアイコンを 作って、それをGMGとかいうツールでまとめることになるらしいです。でもちょっと面倒 なんで、解像度は指定どおりでも色を端折って(8,16,256色で作るので本来は計9種類?) 256色のものだけ準備。GMGはサンプル版をダウンロードして使わせてもらいます。 で、やってみるとようやくいい感じでできあがりました。 でもWindowsXP用という手順で作ったので、Win98で確かめてみるかと別に用意してある Win98マシンにプログラムだけ転送してチェック…あれ?アイコンはいいんだけどプログラム そのものが動かない?!時刻を取得して表示はしているものの、ドット点滅しないし、 キーも押せないし、メニューも操作できん…。 むう、なんで?? まぁそれはそれとして(ぉぃ)、次は本題のダイアログの作成。とりあえずダイアログは リソーススクリプトで作るらしいので、ちょちょいと変更。 絵は、さっきスキャンした絵をまた根性入れて修正し、さらに「マイコン」と「博士」が 行分かれしていたのを連結。少年の絵はゴミとかを修正して、どちらの絵も色をベタ塗りに 塗りなおし。 とまぁ絵はそれなりにできたのですけれど、私としてはダイアログに貼り付けた時に いらない背景は消えていてほしいわけで、白い四角が見えてるなんて論外なのです。 でもBMPって透過色の設定なんてないよね?どうしようか…。 というわけで、アイコンなら透過色の設定ができましたのでそれで挑戦。さっきのGMGで アイコンにしては大きすぎる絵を読み込ませ、とりあえず文句言われずに保存成功。 アイコンはダイアログだとリソーススクリプトで指定して表示させるみたいなので、 そこに指定。 …コンパイルで失敗するなぁ。なんかIDOK付近でエラーとかいうんだけど。 (というか、どういうわけかリンカとリソースコンパイラは日本語のエラーメッセージ 出すんよなぁ) まさか、IDOKを知らないとか?でも方々のサンプルでは特に別なものをインクルードした とか書いてないし…。よくわからんがwindows.hをスクリプトにインクルードしてみるか。 …できた。うーん、それでいいのか?! とりあえずできたプログラムで実験してみると、なんとあの大きな「マイコン博士」の 絵がアイコンサイズに縮小されて表示されている…。ファイルサイズとしては申し分 ない大きさだったんだけど、表示の時に縮小しやがったのか? とするとあとは本当にビットマップを貼り付けないといかんわけで。じゃあ背景透過って どうすりゃいいんでしょ? ならばネットで検索〜っと発見。んーと、まず背景にする絵をSRCCOPYで貼り付け、その 上に描きたい絵のうち背景を隠したい部分を黒、抜きたい部分を白としたマスク画像を 作ってそれをSRCANDで貼り付け、最後に背景を隠す部分(=絵の本体)は元のまま、抜き たい部分を黒にした絵を作ってそれをSRCPAINTで貼り付けるといいそうな。 なんか黒を描くと絵の具の感覚で全部つぶれちゃうような気がしてしまいますが、RGB の値としては(0,0,0)なので、ビット演算するとANDでマスク、OR(この場合はPAINT)では 元の部分が残る、ということになるんですな。今回の場合は背景の絵がないんで、マスク 画像と透過部分が黒の絵だけを使えばいいということになります。 その絵ができたところで、今度はダイアログにいつ描くか?ということになるのですが。 なんかWM_INITDIALOGというメッセージがダイアログ作成時に飛んでくるらしいので、 その時に描画操作をするようダイアログのプロシージャに記述することにします。 で、結果は…う、何も描かれない。やっぱダイアログは普通のウィンドウとは違うんです かねぇ? では、とDialogBox()ではなくCreateDialog()で作成するよう変更してみると、なんと メニュー操作を行おうとした瞬間プログラムが凍ってしまいました。むむう、CreateDialog() の解説があまりなかったのでけっこう適当だったんですけど、生半可にはいきませんねぇ。 DialogBox()に戻しておいていろいろ調べてみると、WM_ERASEBKGNDというメッセージで描くと いいらしいので、メッセージだけ書き換えて実行…おお、背景透過で出るにはでましたけど、 ダイアログがまるごと透過してて下(メインウィンドウ)まで丸見え、いやそれをまとめて背景に したダイアログができてしまいましたよ。マスクとかは問題なさそうですけど、やっぱりなんか 変です。 で、もうわかんないのでWM_PAINTメッセージで描くように変更。かなりヤケだったんですけど これが正解でした。なんてこったい。普通のウィンドウなんですなぁ。 2004.5.30 で、昨晩はメドが立ったので安心して就寝、翌日作業再開。 といってもダイアログの完成だけ。まぁいろいろいじって、特にリソーススクリプトと 実際のプログラムで座標系が変わっちゃうのがナニなんですが、これで以前から考えて いたデザインのダイアログが完成。うっふっふ。 あとはあまり作業せず、ちょっと書籍の買出し。高かったけど、プログラミングWindows 上下巻買っちまいましたよ。でも親からの頼まれ物も含めて本12冊は重かった…。 2004.6.1 自動演奏の検討。 音はとりあえず作れているので、テンポごとに必要な音を出し、次のテンポで前の音を 止めて次の音を出す、という繰り返しが素直な感じ。 テンポはMZ-40Kでは速度を示す数値を入力することになっていて、それぞれのスピードが 数値に対応付けられています。数値がいきなり速度を表しているわけではない、というのが ちと面倒。でも、昔のことですからなんか数学的法則がありそうです。むう…。 ♪=450というのは、四分音符が1分間に450回鳴らせるということですよね。これが最速 のテンポで、表現できる音は三十二分音符までですから、それを最小単位と考えると 四分音符ひとつで三十二分音符8つ、つまりテンポ0だと450×8=3600…。おお、なんか ありそう。 その1/60が1秒間に鳴らせる数なので、併せて計算するとこんな感じ。 60Hz 32分 ÷60 50Hz 32分 ÷50 0 450 3600 60 375 3000 60 1 225 1800 30 187 1496 29.9 2 150 1200 20 125 1000 20 3 112 896 14.9 94 752 15 4 90 720 12 75 600 12 5 75 600 10 62 496 9.9 6 64 512 8.5 53 424 8.5 7 56 448 7.5 47 376 7.5 8 50 400 6.7 42 336 6.7 9 45 360 6 37 296 5.9 A 41 328 5.5 B 37 296 4.9 完全な規則性はないようですが、電源周波数がいくつであっても音を出す周波数は 一定らしいことがわかりますね。 で、最短は60Hzということは周期が16.67ms。プログラミングWindowsを見てみるとWin98 なんかでは55msが最短のタイマー周期とのこと。するってぇと、テンポをタイマーで 実現する方法では速いテンポを表現できないということになりますね。 うむむ、だったら、タイマーではなく音の終わりのイベントでやるしかないかな。つまり 四分音符の音は四分音符の長さのデータを用意して鳴らす、ということ。でもなんか いっぱい変数がいりそう…。 2004.6.4 なんとなく平日は作業が進まなくて、でもモチベーション低下を防ぐべく現バージョン で遊んでいたら、速いクリックの入力が通らないのが気になる気になる。いや、前から 認識はしていたんですが、どーしても直したくなって。 で、間隔をあけて押すと入力できて、あけないと入力できないってのは、この時間間隔 からしてダブルクリックと思われているような感触。いろいろ文献を読んでみると、 特に指定しない限りはダブルクリックがシングルクリックに分解されてメッセージとして 伝わってくる、とあるんだけど、どうも様子が変。 ということで、試しに親ウィンドウのウィンドウクラスの登録の時にスタイルとして CS_DBLCLKSも指定して、サブクラス化してある各ボタンのプロシージャの中でWM_LBUTTONDBLCLK もWM_LBUTTONDOWNと同じ処理をするようにしてみると、見事に細かい入力も全部区別して 処理できるようになりました。 でも、ダブルクリックに相当する入力ではWM_DRAWITEMメッセージが飛んでないらしく、 ボタンが押された旨の描画がありません。ボタンを作る際のCreateWindow()のスタイル指定 でBS_NOTIFYを並べても変わらず。むう、オーナードローボタンってダブルクリックを 受けられないのかなぁ? 2004.6.5 ダブルクリックが受けられない件について調査継続。といっても全く成果なし。ちょっと TK-80シミュレータの動きを見てみたら、見事にダブルクリックを無視していたので こちらもまぁとりあえずは許してもらおうかな、と妥協の境地。入力できないよりはいい ぢゃん。 で、自動演奏の続き。どうせなら、曲の最大演奏時間分のバッファを用意しておいて、 演奏開始と同時にすばやく全曲データを作成し、単なるリピートで全部を演奏すれば簡単 じゃないかと思い、まずはオルガン用データの作成しなおし。 オルガン用音データを全音符の長さだけ各音階について用意し、曲データを作る時に必要な 長さ分だけ音データからコピーしてくればいいよね、という考え方で、とりあえず一番 遅いテンポの全音符の長さを計算してみると 1/5×32×8000=51200 ここから、登録できる音の数をかけると (128音入れられますが、1回繰り返せますので、繰り返し指定を除く127音の倍) 51200×254=13004800 → 約12MB むう、たかが自動演奏でけっこう食いますけど、しかたないか。 ということでオルガン用データを作成するんですけど、それまで周波数から周期データを 出していたのが、データ作成のやりやすさからH時間とL時間を指定することに変更しました。 で、その周波数から手計算していくと…高音のラあたりから一周期の時間が短くなりすぎて 違う音階なのに同じデータになるものがあることを発見。むむう、まずいですね。 かくなる上は、サンプリング周波数を上げてちゃんと違いの出るデータにするのが一番。 11.025kHzでは8kHzとの違いが少ないので、思い切って22.05kHzに変更。さっきの計算を こちらに適用すると、一番長い音は 1/5×32×22050=141120 曲のために用意するバッファは 141120×254=35844480 → 約34MB んんん、厳しいけどなぁ。まぁがんばって作りましょう。 …。 で、できたデータに対して音の周期と処理単位の8(バイト)の公倍数になるよう長さデータ を作り、これをヘッダに登録してオルガンの再実装は終了。 今度は、いろいろなテンポでのそれぞれの音長が何バイトになるか、そのデータを作って 曲データ作成に備えます。 2004.6.6 えっと、作成の続き。なんか冴えない頭でやっていたせいか間違いが多いんですけど。 …。 うん、つまり最長データは903168バイト、と。 …あれ?えーと、そういえば昨日一番遅いテンポの全音符って141120バイトになるって計算 してなかったっけ。なんか増えてるやん。 うー、あっそうか、1秒間の長さ=22050を使わないといかんのに、ここに141120を使ってた のか。そりゃおかしいわな。 …と、ふとマニュアルを見ていると、テンポを説明する表には0〜Bの分しかないんですけど、 使用方法の説明のところでは0〜Fと書いてあるのを発見。えー、これってもしかして純粋に 計算で作ってます? …あっ!!わかった。上から1/2、1/3と分母が増える割り算をしてるんだ。つまりこう。 0 1/1 60 1 1/2 30 2 1/3 20 3 1/4 15 4 1/5 12 5 1/6 10 6 1/7 8.5 7 1/8 7.5 8 1/9 6.7 9 1/10 6 A 1/11 5.5 B 1/12 5 C 1/13 4.6 D 1/14 4.3 E 1/15 4 F 1/16 3.7 実機で確かめても、テンポBよりもテンポFは間違いなく遅いです。むむむ、計算しなおしだ なぁ。 そういえば、音階のところも00〜0B、10〜1Bときて2xを飛ばして30〜3Bとなってるのもなんか 怪しい。まさか…と周波数を見てみると、これもただ1/2、1/3…と計算されているに過ぎない ことが判明。H時間だけで計算してみるとこんな感じ。 0 21 1 42 2 63 3 84 4 105 5 126 6 147 7 168 8 189 9 210 A 231 B 252 C 273 D 294 E 315 F 336 おお、なんか8ビットの数を超えるよね?とか思ったけれど、特には関係ないのでした。この ように計算されるデータを作って、オルガンのほうは確認OK。 それにしても、最長32分音符が3.7Hzということは、最長音の長さは 1/3.7×32×22050=188160 曲データの大きさは 188160×254=47792640 → 約46MB さらに、3オクターブだけでなく(音階として成立しなくても能力として)16オクターブ発音 できるとなると、その分だけでもまた膨大。うーん、曲バッファ方式はあきらめますか。 2004.6.8 自動演奏の続き。 一応全音符のデータは揃っていることになっているので、音を鳴らす時、バッファとして 全音符分を指定してもデータ数を必要な音長に制限することで以前考えていたことが実現 できることに気づきました。指定したデータ数だけ発音したらMM_WOM_DONEメッセージが 飛んでくるわけで、そこで次の音を出すようにすればいいんですよね。 えー、最初の音しか出ません…。メッセージは?? 連続するようにはなりましたが、違う音階にしても同じ音しか出ませんよ? あ、オクターブを変えるとそれなりに変わりますね…。 ああ、やっとうまくいくようになったか。 でも休符用データがうまく指定されないな…。 2004.6.11 休符が休符でなく「ブ〜」とか低い音が鳴るので、休符の"音"(つまりは無音データ)が 間違ってるのかしらん、ということでオルガンで「鳴らせる」ようにしたら、ちゃんと 「無音が鳴っている」らしいことを確認。まぁ鳴らすのに失敗しても無音なんですけど。 で、よ〜く考えてようやく原因判明。 例えば高音だと0C〜0Fまでは音として未定義で、実機では0Bと同じ音が鳴るようにして あることから、こちらでも0C〜0Fが指定されたら0Bになるように細工していたわけですね。 休符はEEなので、でも休符のための無音データは2Fに作ってあったので、それに変換する よう仕掛けてありました。 ところが、xC以上でxBに変換する機構が先に働いていたため、EEはEBに変わり、本来は 休符だったものがずいぶん低い音として鳴っていたわけですね。 ということで、これを直してようやく自動演奏が形になりました。 2004.6.12 音長についてもよく見ると、単に32分の長さを整数倍しているだけであることに気づき ました。ですので、マニュアルには未定義の音長のデータを作成。わざわざ検証までは しませんけどね。 それと自動演奏の忘れ物、リピートを実装します。途中に戻って、また同じところに来たら 今度は戻らない、というやつ。これはフラグを設けて、一度戻ったらフラグを下げて、 フラグが下がってたらもう戻らないようにします。自動演奏では曲全体が繰り返される のですけど、その繰り返しでフラグがリセットされないミスを見つけて、あっさりと 実装。 先日は自動演奏のテストにマニュアルの「こいのぼり」を打ち込みましたが、いちいち それは面倒なので、スクリプトから入力できるようにします。できるのはアドレス指定と データ書き込みのみ。使い方は手打ちに準ずるのですが、表示させないんだから読み出し は必要ないですよね。 ファイル読み込みはCではfopen()とかいろいろやるもんですけど、プログラミングWindows を見てみるとちゃんとWin32のAPIとしていろいろ整備されていました。そこで、ダイアログ の解説にあるサンプルを参考にスクリプト読み込みを実装。まぁコモンダイアログで ファイルを指定させて、CreateFile()でオープンし、ReadFile()で一気読み込み、という 手順ですかね。 スクリプトはテキストならどんなのでも読み込みますが、先頭が"MZ-40K"で始まり、そこから 任意のコメントを記述して、"---"を見つけた次から読み込みスタート。手抜きなので16進 の数値と@とWとwを避ければ関係ないことを書いてもかまいません。 実装にあたっては、メモリだけでなく他の制御レジスタも指定できるようにします。 つまり曲データとして曲本体と格納位置とテンポがひとつのファイルで記述できるように するのです。便利だと思いません? ひととおりの実装を終え、「こいのぼり」でデータを作りつつ、一気読み込みする先の メモリの開放をするのを忘れていたので追加しつつ、とりあえず完成。 まぁ簡単なスクリプトなので、最初こそいくつかミスはありましたが比較的簡単に動かせて しまいましたよ。「こいのぼり」を読み込ませて、RUNキーを押すだけで、演奏開始。 よしよし。 が、しかし。 実は実装直後、なんとプログラムが動かなくなってしまったのです。時計表示の秒の点滅 さえ起こらず、ウィンドウは出るもののそのまま固まる状態。追加したものをコメントアウト していっても元には戻らず、試行錯誤の末タイマを起動させないと固まらなくなることは 確認できました。その状態で、上記のスクリプトのデバッグをやったわけです。 で、どうにもわからないのでソースコードデバッガでステップ実行。 …。 …。 …。 んー、WM_TIMER受信で秒が点滅する時の、描画を指定する矩形を保持する変数lpRectの アドレスが0x00000001番地とか言ってるなぁ。むぅ? そーいえば、これポインタとして宣言してるのに、そのアドレスの初期化をしてるとこって どこもないよね?? もしかして、今まで動いていたのは偶然?!うーん、なんてこったい。ポインタでなく、 普通のRECT型の変数として宣言するとうまく動くようになりました。うわ、これあちこちに あるんですけど…ということで大急ぎで修正。 2004.6.13 あとはメモリ内容の書き出し。当然、スクリプト形式で書き出します。書籍を参考に、 ダイアログから書き出しまでを実装。データ作成中にその文字数を数え間違ってヌルが いくつも混じったりしましたが、それなりに素直に完成。 ここまで作ると、そろそろドキュメントが必要かな?ということで書き始めました。 溜めすぎたかな?あるいは書きすぎ?すぐには終わらないんですけど…。 2004.6.19 夏コミOFF用デモマシン、MZのMであるところのMebius MURAMASA PC-CV50FWを購入。 早速WinXPの領域を減らして別のOSを…と思ったらリカバリCDでは領域変更できないし、 システムセレクター2で領域の大きさを変えると起動できない罠。で、大きさの変わった Cドライブに対してリカバリをかけるとどうなるか試している間に、ドキュメントを 書き書き。 それと、音楽演奏で開始番地に終了コードがあると永久ループに入り暴走状態になる のを「再現」。まぁそのままだと見た目の違う永久ループだったこともあるし、本物では 電源再投入が必要な状態なので、メニュー追加で対処。 で、かなり時間が開いたけど4thリリースです。 2005.5.1 4thリリースから10ヶ月以上。MZ本の原稿書きとか、いろいろあってすっかりストップ しておりました。今も実は別のネタを仕込み中なのですけど、部品入手の問題で中断中。 何か作って前進感(なんて言葉はないんだが、感覚的には「おねティ」のテーマと同じだな) を味わうべく作業再開。 とりあえずは、あれからの話を。 4thリリースの状態でMZ-MLのオフ会に持ち込みました。いたく感動していただけました。 せっかくの手描きの絵なので、PhotoShopのデータも提供。尊敬の念を込めて「あんたアホや」 とコメントしていただけました。 オフ会直前、別のネタを思いつきましていろいろ試していたのですが、市販DLLが Open Watcomではリンクできないという問題が発覚し、結局は最初見送っていたはずの VC++ Toolkit 2003を導入することになりました。IDEがないんですけども、Borland C++に 使えるCPadなるフリーのIDEを流用することで、ひとつの窓に全部が収まる環境を構築 することが可能になりました。まぁその時のネタは別の問題から長期保留中なワケです けれどもね。 で、今回もVC++ Toolkit 2003で作成すべく、環境を移行。マルチメディアライブラリの リンクとULONG_MAXが未定義だったのを除けば特に問題なくリコンパイルできました。 あと、書籍「Win32 API リファレンス」も購入しました。MSDNに入らず、いかにして 必要な情報を揃えるか…というところなのですが、この本でも解説されないAPIがあるんです ねぇ。とりあえずWin32SDKのヘルプを使えば全部読めそうなのですが、こちらは英語なのが ちょっとね。 mz40kwinも中断後いろいろ考えていまして、それがなんとなく解決しそうな見込みとなった ので再開した次第。その考えとは…。 ・ゲームスイッチをどうしよう? 本物のMZ-40Kには「ゲームスイッチ」という、特定のメモリを連続で読み出して表示する ものがあります。といっても裸のマイクロスイッチがコードの先にぶら下がっているだけの ものなんですが、これをWindowsの画面にどのように表現するかで迷っていました。まぁ ボタンを表示した小さい窓を用意しておくのが素直かもしれませんが、なんだかなぁ〜 と考えていたところ、そう、マウスのクリックにすりゃいいんぢゃんということに気づき ました。操作感覚としてもピッタリ、ではないかと。 ・上の白いところが気になる〜 ボリュームスイッチがハミ出るのでしかたないんですが、メニューと絵との間に白い隙間 がありますよね。どうもかっこ悪いんでなんとかしたかったんですが、ようやくメニューや 枠なしのウィンドウに変えようかと考えています。メニューは絵の上で右クリックすれば ポップアップメニューが出るとすればいいでしょう。まぁ使用上どうでもいいことなので、 やらないかもしれませんが。 まずはリハビリに、時計機能の修正。そもそもは起動直後時計は0時0分からスタートする わけで、キー入力で時刻修正するようにしたのと、実際には面倒なのでメニューから 時刻設定ができるように変えました。あまり苦労せず(するはずないと思ってたけど)、 修正完了。 次はタイマーを…と思って実機で確認中、なんと23:59→00:00に繰り上がる時、10秒間だけ 24:00を表示するのを発見。どんな仕組みで動いているのやら??というわけで、時カウンタ を24から0に変更する箇所を移動し、10秒だけ猶予を与える仕組みに。