気まぐれな戯れ言 バックナンバー6


気まぐれな戯れ言バックナンバーです。全体の一覧はこちら

03/06/29 ベジェ曲線の長さ編
03/06/14 立体視できる3次元物体編
03/04/02 CodecをつかったMP3のデコード・再生編−1
03/03/14 簡易OSを作ろう編−3

03/06/29 ベジェ曲線の長さ編[★★◎◎◎◎]
とあるメーリングリストで出てきた話題なのでここで取り上げて見る。

ベジェ曲線はグラフィックの分野以外にもCADでも使われているらしい。
グラフィックならともかく、CADで使うとなるとその長さを求めてみたくもなるが単純な円弧でもないしどうやって曲線の長さを求めればいいんだ?という話。

一応数学的な部分についてはHK WORLDで述べられているので、これでわかる人はもうプログラムを組めるはず。
ただ、このサイトには実際のプログラムがないので一応ここでだらだら書いてみる。

とりあえずベジェ曲線そのものについて。

ベジェ曲線はn次ベジェ曲線と言われて制御点の数で色々なものが有るが、一般的に使われるベジェ曲線は3次ベジェ曲線である。
これは、始点・制御点1・制御点2・終点の4点からなる曲線で、終点を次の曲線の始点とすることで連結させると複雑な曲線も書く事が出来る。
Illustratorなんかのツールだと、ある始点の制御点として、その前の曲線の制御点2と、これから書く曲線の制御点1がくっついた形になっているものが多いと思う。
(始点から滑らかさを示す接線っぽい奴が伸びてるというか・・・)
前の曲線の制御点2、終点(=次の曲線の始点)、次の曲線の制御点1を1直線に並べれば連結部分も滑らかに繋がるし、Illustratorとかで適当に線を引くとそんな感じで制御点が取られる。

んで、実際のベジェ曲線の数学的表現について。
3次ベジェ曲線は、始点・制御点1・制御点2・終点をP0,P1,P2,P3とすると、媒介変数tを用いて以下のように表す事が出来る。

P(t) = (1-t)3*P0 + 3t(1-t)2*P1 + 3t2(1-t)*P2 + t3*P3

上の式をx、y(3次元ならzも)についてt=0〜1で変化させていって得られるP(t)をドットで打って行けばよい。
ここでとりあえず特徴として、P(0)=P0、P(1)=P3、すなわち始点及び終点は名前の通り必ず通り、始点と終点となることがわかる。
一方制御点は必ず通るとは限らない。(というか4点を直線上に配置でもしないとそうそう通らない。別に通ったからって何の問題もないと思うけど)
↑の式のままだと扱いにくいので、tの次数で並べ替えてみると、

P(t) = A*t3 + B*t2 + C*t + D

A = -P0 + 3P1 - 3P2 + P3
B = 3P0 - 6P1 + 3P2
C = -3P0 + 3P1
D = P0

上の式は、x,yそれぞれについて成り立つので、2次元ならA,B,C,Dもx,yの2値のベクトルになるし、3次元なら3つになる。
つまり、2次元ならばA,B,Cをそれぞれx,yについて求めたものを構造体風にA.x,A.y,B.x,B.y,C.x,C.yの様に書くとして、

A.x = -P0.x + 3P1.x - 3P2.x + P3.x
B.x = 3P0.x - 6P1.x + 3P2.x
C.x = -3P0.x + 3P1.x
D.x = P0.x

A.y = -P0.y + 3P1.y - 3P2.y + P3.y
B.y = 3P0.y - 6P1.y + 3P2.y
C.y = -3P0.y + 3P1.y
D.y = P0.y

のような感じで求めればよい。

上の式は単なる3次式なので、後は高校の時習った曲線の長さの公式が使える。
媒介変数を使った場合の曲線の長さLの式は、t=a〜bとすると、

L = ab √((dx/dt)2+(dy/dt)2) dt

となる。(式が見づらい場合は上のHK WORLDで。)
3次元、4次元・・・ならルートの中に(dz/dt)2でも(dw/dt)2でもどんどん付け加えて行けばよい。
要は何をやっているかといえば、dtだけ変化した場合のP(t)とP(t+dt)の2点間の直線距離を採っているだけですね・・・

じゃあ、上記の積分計算をどのように行うか。
とりあえずP(t)を微分して、さらに二乗してみると、

P'(t) = 3A*t2 + 2B*t + C
(P'(t))2 = 9A2*t4 + 12AB*t3 + (6AC + 4B2)*t2 + 4BC*t + C2

となる。上の式はx,yそれぞれに対して成り立つので、P(t)のx成分をP(t).xと表現するのであればd(P(t).x)/dt=P'(t).xと書くことが出来る。

P'(t)にはDが出てこないのでA,B,Cだけ求めればいいわけだが、(dx/dt)2=(P'(t).x)2なので、

(dx/dt)2 = 9A.x2*t4 + 12A.x*B.x*t3 + (6A.x*C.x + 4B2)*t2 + 4B.x*C.x*t + C.x2
(dy/dt)2 = 9A.y2*t4 + 12A.y*B.y*t3 + (6A.y*C.y + 4B2)*t2 + 4B.y*C.y*t + C.y2

上の式は見てのとおりそれぞれx、yについて対称。3次元になったら(dz/dt)2も同じ様に計算すればよい。

ここで積分すべき関数をQ(t)とおくと、

Q(t) = √((dx/dt)2+(dy/dt)2)
= √(9*(A.x2+A.y2)*t4 + 12*(A.x*B.x+A.y*B.y)*t3 + (6*(A.x*C.x+A.y*C.y)+4(B.x2+B.y2))*t2 + 4*(B.x*C.x+B.y*C.y)*t + (C.x2+C.y2))

まぁみてわかるとおり.xが由来の項と.yが由来の項が足し算になっているだけなので、3次元でも.zの部分を同じように足せば大丈夫。

このQ(t)の中身は√の中に4次式が出てきていることになる。
これの不定積分ができるとありがたいのですが、どうなるかわからないので数値積分をしてみる。

数値積分には色々方法があるが、ここでは一番簡単な前進オイラー法を用いる。

一応前進オイラー法について解説。
先ほどの曲線の長さの式で、tの積分区間を0〜1にして計算すればいいが、ここであえて0〜sとおいて

R(s) = 0s Q(t)dt

とおくと、R(1)が求めるベジェ曲線の長さである。
しかし、Q(t)の不定積分が直接わからないので、Δsずつ刻んで求める。

R(s+Δs) = 0s Q(t)dt + ss+Δs Q(t)dt

ここで、前の項の0s Q(t)dt=R(s)である。 一方後ろの項に関してはΔsが十分小さいならss+Δs Q(t)dt ≒ Q(s)*Δsと近似できるので、結局

R(s+Δs) ≒ R(s) + Q(s)*Δs

とすることでR(s+Δs)を近似的に求める事ができる。
あくまで近似なうえ、Δsを十分に小さく取らないと誤差が大きくなる。
幸い今回はQ(t)が単なる2次以下(4次式が√の中なので)の式なのでそこまでΔsを小さくしなくても0.001ぐらいに取ればよい。

ここまで聞くと難しく思えるかもしれないが、ゲームで「落下の加速度が1フレーム0.1ピクセルだから毎フレーム落下速度を0.1足して、今のy座標に落下速度を足す」なんてことをしてる人は多いと思う。
ほんとは加速度とか使うんならy座標は時間に対して正確に二次的に増やさなきゃいけないけど、そこまで
せず毎フレーム0.1を足すだけ、速度を足すだけだと思う。
「60FPSでは加速度秒間6ピクセル(6/60=0.1)」と考えれば上の計算は「加速度を積分して速度を求める」「速度を積分して座標を求める」と言うことを前進オイラー法で求めている事になる。

ちなみに、より精度が高く、発散しにくい数値積分の方法としてルンゲ・クッタ法などがあるので興味がある人は調べてみるといいかもしれない。
積分がからむような物理シミュレーションなどで広く使われているはず。

まぁごたくはいいとして、上記の計算を実際にプログラムに起こしてみるとこんな感じ。

'dt
Const NUM_DIV = 1000

Function GetBezierLength(ByRef p() As POINTAPI, Optional div As Double = NUM_DIV) As Double Dim fct(2, 1) As Single Dim fct2(4) As Single Dim i& Dim t#, f#, s# 'P(t)=(1-t)^3*P0 + 3t(1-t)^2*P1 + 3t^2(1-t)*P2 + t^3*P3 ' =At^3 + Bt^2 + Ct + D 'とすると 'A=-P0 + 3P1 - 3P2 + P3 'B=3P0 - 6P1 + 3P2 'C=-3P0 + 3P1 'D=P0 'P'(t)=3At^2 + 2Bt + C '(P'(t))^2 = 9A^2t^4 + 12ABt^3 + (6AC+4B^2)t^2 + 4BCt + C^2 'X及びYに対応するA,B,Cを求める fct(0, 0) = -p(0).X + p(3).X + 3 * (p(1).X - p(2).X) fct(1, 0) = 3 * (p(0).X + p(2).X) - 6 * p(1).X fct(2, 0) = 3 * (p(1).X - p(0).X) fct(0, 1) = -p(0).Y + p(3).Y + 3 * (p(1).Y - p(2).Y) fct(1, 1) = 3 * (p(0).Y + p(2).Y) - 6 * p(1).Y fct(2, 1) = 3 * (p(1).Y - p(0).Y) 'P'^2の係数を求める fct2(0) = 9 * (fct(0, 0) ^ 2 + fct(0, 1) ^ 2) fct2(1) = 12 * (fct(0, 0) * fct(1, 0) + fct(0, 1) * fct(1, 1)) fct2(2) = 6 * (fct(0, 0) * fct(2, 0) + fct(0, 1) * fct(2, 1)) + 4 * (fct(1, 0) ^ 2 + fct(1, 1) ^ 2) fct2(3) = 4 * (fct(1, 0) * fct(2, 0) + fct(1, 1) * fct(2, 1)) fct2(4) = fct(2, 0) ^ 2 + fct(2, 1) ^ 2 '数値積分 s = 0 For t = 0 To 1 Step 1 / div f = Sqr(fct2(0) * t ^ 4 + fct2(1) * t ^ 3 + fct2(2) * t ^ 2 + fct2(3) * t + fct2(4)) s = s + f / div Next GetBezierLength = s End Function

上記のプログラムは、引数p()にp(0)〜p(3)に始点・制御点1・制御点2・終点が入るようなPOINTAPI構造体、divに分割数(=1/Δs、デフォルトでNUM_DIV=1000)を入れるとベジェ曲線の長さを求める。
これはPOINTAPI構造体を渡すんで整数かつ2次元だけど、3次元や浮動小数にするのは簡単だと思います。
分割数は10とかだと少ないけど、100以上ならそれなりな値が出るはず。
100〜10000だと1%程度の誤差だった。
みてわかるとおり数値積分が単純なので、ここをルンゲ・クッタ法とかにすればさらに精度が上がるかな?
そのうちVB講座で動くプログラムとして公開します。

しかし思ったより文章長くなったなぁ・・・
HTMLは数式打ちづらい。TeXコードIncludeとかできないかな^^;

03/06/14 立体視できる3次元物体編[★★★◎◎]
ゲームネタがパクリっぽいSTGとオリジナルと思われるパズル系でいくつかあるがあんまり作る気が起きないなぁ・・・
今回戯れ言一覧とか作ったら年々記事数が減ってきてるのが気になる。
まぁその分最近のは1回分の文章量が多いってのはあるけど。
小ネタでもいいからちょくちょく書かないとな。


卒論のときOpenGL使ってたけど、OpenGL+GLUTだとちょっとした3Dのプログラム組む時便利だね。
初期化の手間もDirectXとかに比べればそんなにかかんないし、別にゲームとかに使うわけでもないならほんとに最低限の部分だけ初期化すればいい。

まぁそうは言ってもゲームに使う気は起きないな・・・
少なくともGLUTの機能だけでは不満。
テクスチャとかもintやcharやfloatなど色んなデータで入力できるけど変換とかで遅そうだし。


というわけで、OpenGLでちょっとしたプログラムを書いてみる事に。
近年3次元ディスプレイなるものが色々研究されてきてるらしい。
子供の頃に、少しずれた赤い線と青い線で書かれた絵を赤と青のフィルムが貼ってあるメガネを使う事で3次元に見えるという体験をした人もいるかもね。
あとは、最近でも書店などで見かけるのは、ノイズが載ってるだけのような絵を寄り目などで焦点をずらす事で奥行きのある模様が見えるという奴ね。
(そう言えばVirtualBoyはこの手法使ってるのかな?それとも単にディスプレイが多層になってるのかな?)

なぜこれらの手法で奥行きが感じられるかを考えてみる。
心理学的な要素としては、物が3次元に見える原因として物の影(影の位置やかかりぐあいで前後関係がわかる)や模様(模様がだんだん小さくなる→だんだん遠くの面になる)などがある(らしい)。
これらは別に目が2つでなくても奥行きが感じられる原因となる(らしい)。
片目つぶると遠近感が少しわかりにくくなるが、先入観などもあって小さいものや動くもので無ければそれほど遠近感に狂いが出ないのはこれらのせいなんでしょう。

で、さらに光学的に考えれば奥行きがわかるのは正に目が二つあるからであるといえる。
目が二つ別々の位置にあるので(まぁ「二つ」と言うくせに同じ場所にあるわけは無いが)当然二つの目には少し位置のずれた画像が入る事になる。
そこで脳が自動的に(←これ結構すごいと思う。2点からのステレオ画像生成をコンピュータで行うのは結構難しい。まぁ目で見る場合はそれほど正確な奥行きを求めてるわけではないんだろうけど。)そのずれから奥行きを計算してると考えられる。

んで、なんでいわゆる(ノイズから絵が浮かぶ系の)ステレオグラムが立体的に見えるか考えてみる。
(以下、個人的な想像であり解釈であるので間違ってるかもしれないです。)
ステレオグラムは単なるノイズであるが、当然ながら左右の画像のノイズは基本的に重なるように出来ている。
ただ、一部奥や手前に持っていきたい部分は少しだけ左右の距離が周りとずれている事になる。
(寄り目じゃなくて普通に画像を見ると、へこんだり浮かんだりする部分の境目がよく見るとノイズがその形に線が入ったりして区切れてるのがわかる。)

例えば、2つの同じような画像が左右に10cm離れていて、それを20cm遠くから見ているとする。
交差法では、大抵この左右の画像が重なるように寄り目で焦点を合わせようとするだろう。
そこで、画像の一部分だけ10cmでなく9.9cmだけしかずれていなかったらどうなるか。
寄り目をしたときに周りの部分はぴったり重なるが、一部分だけは1mmだけずれて見える事になる。
この時、単に2つの同じ画像が1mmずれて見えるというわけではなく、脳がその1mmの誤差を修正するために
「この領域は少し手前にあるからずれが小さいんだ」
と見ることで、この領域は10:9.9≒20.2:20で2mm奥にあるように見える事になる。
(↑ここの計算が間違ってるかも知れないけど、まぁこんな感じで手前にあるように見えるという事で。)
遠くに見える時も同様に10cm以上の距離がある部分は手前にあるように見える。
まぁ考えかたとしては、
「周りよりずれが大きい→手前にあるからだ」
「周りよりずれが小さい→奥にあるからだ」
となります。
ずれが数cmと大きくなると、もはや脳は両者は重ねる事が出来ず、別々の模様と考えるので絵は重ならなくなります。


んで。
別にノイズから模様が見える必要はなくて、この手法は普通の物体を3次元に見えるようにも応用できます。
というわけで、2つの目から見た物体を描画してみる。
まぁ色々書いたけどプログラムとしては大した事ない。
(OpenGL+GLUTで作成)
大きさ1のティーポットが距離5の位置にある場合、0.4離れた二つのカメラから見た状態の絵を描いて見る。
横に並べると、下のようになった。



ティーポットの取っ手の部分が少し手前にあるように見えないだろうか?

作成したプログラムは、プログラム実験部屋に置いてある。
ティーポットが回転するだけであるが、(寄り目で見つづけるのはちょっと大変かもしれないが)ティーポットの奥行きが感じられて面白いと思う。


ちなみに、この技術はVirtual Realityや3次元モデルの直感的理解のため、3次元ディスプレイ等の分野で応用が考えられているらしい。
ずっと寄り目で物体を見るわけにも行かないので、両目に別々の画像を表示すればいい事になる。
最初に書いた赤と青のフィルムを貼ると言うのが一番簡単な方法だと思われるが、この手法だと色が表現できない。
手っ取り早いのはヘッドマウントディスプレイ(HMD)で両目に別々の画像を映せば簡単である。
この方法は全員がHMDをつける必要があるので、大勢に見せたい時は頭が重いし値段が高い。
赤と青のフィルムの応用として、光の偏光を利用する方法がある。
ディスプレイからは左目用と右目用でそれぞれ縦と横に光の偏光の向きを揃えて表示する。
両目のレンズはそれぞれ縦と横に偏光した光だけを通す事で、色の問題も解決できるし、見る側はメガネをつけるだけでいいので負担も少ない。
ただ、偏光するとなると現在だと液晶を利用する事になるのかな?
液晶ディスプレイの大型化は技術的にもかなり難しいらしい。

将来漫画やSF小説であるような空間上に表示でききるような立体ホログラム装置とかできるのかな・・・

今回あんまりプログラムに関しては細かいこと書いてないな。。。ごたくばっかりだ。
03/06/28補足

音についてもやはり2つ耳があるというのは大きいらしい。
ただ、画像の場合と違い、通常音は鼓膜の振動という情報しか耳に入らないわけで、単に二つの耳に違う場所からの音が入るから場所がわかるというわけでもない。

とりあえず2つの耳は異なる場所にあるので、目同様に音の聞こえるタイミングの遅れや、音量差で距離がわかるとか。
ただ、両耳から距離がわかったところで、それぞれの耳からある大きさの球を考えればその交差部分は大抵円になるわけでまだ1次元分(円自体は2次元だが円周部しかないので)の自由度が残ってる事になる。
んで。
どうやら、マネキンの両耳にマイクをつけて録音したのを聞くと、かなり位置がわかるらしい。
単に二つのマイクからステレオの音声を採っただけだとだめなんだけどね。
と言うのも、頭自身で音が遮られて音が回折して耳に入るのを脳が自動的に計算しているんだそうな。
人間の脳って凄いね。。。

脳の計算といえば、化粧やらメガネやらしていて、表情がコロコロ変わってもそう他人と間違える事はないが、これをコンピュータで計算でやらせるのはかなり難しい。

03/04/02 CodecをつかったMP3のデコード・再生編−1(ファイルからの必要なデータの取得)[★★★◎◎]
以前からちょくちょく挑戦していたCodecのMP3デコードです。
あと、waveOut系関数をつかって任意の音声をストリーム再生する方法も知りたかったので色々調査。
ところでCodecってCOder-DECoderの略だとか。MOdulator-DEModulatorみたいだな。

まずはMP3のデコードについて。
最近はフリーウェアですらMP3をBGMにつかってるゲームがちょくちょくある。
一応Codecが入っているとMP3形式のwavはmciSendSoundで"play filename.wav"で充分再生出来るんだけど・・・
まぁBGMとしてつかえるか微妙だしDirectSoundに応用できなさそうなんで、とりあえずデコードだけをしたいと。
で、大抵はデコードルーチン自体をプログラムが持ってるか、デコード用のDLLとかを利用している事が多い気がする。
ただ、せっかくOSがAudio用Codecを持っているので、それを利用する事を考えてみます。

個々のCodecを統一的に利用するために、マルチメディア系関数としてAudio Compression Manager系関数acmXxxxが色々と定義されてます。
midiOutXxxxとかwaveOutXxxxと同様にwinmm.dllで定義されているらしく、wavOutXxxxとかと関連する構造体も多いようです。

特に変換の場合はacmStreamXxxxを使うことになります。
これをつかうとCodecさえあればこの一連の関数だけでエンコード・デコードできて結構オトクだと思います。
この部分はプログラム中にデコーダのコードを持つとか個別のDLLをつかうとは別の有利な点でしょう。

一応先に書いておくと、acmStreamXxxxをつかうためにはWAVEFORMATを拡張したWAVEFORMATEXと言うものが必要です。
特にMP3の場合にはWAVEFORMATEXをさらに拡張したMPEGLAYER3WAVEFORMATが必要。
ちなみにこの構造体はVC6ではmmreg.h内で宣言されています。
少なくともVB5のAPIビューアーにはありませんでした。(まぁVB5は97年に出たしな・・・)
逆に、変換元と変換先のWAVEFORMATEXを取得できればあとはacmStreamXxxxが勝手にコンバートしてくれると言うありがたい仕組み。

とりあえずacmStreamXxxxの前に、WAV形式のMP3データにしろMP3ファイルにしろ名前のウェーブデータ部分だけをまずファイルから取り出す必要があります。

まず、WAVファイルのヘッダは以下の通り。
(4byte)'RIFF' - 4バイトのヘッダ
(4byte)これ以降のファイルサイズ - ファイルサイズ-8の値
(4byte)'WAVE' - waveファイルであることを示す

以下は、
(4byte)タグ名
(4byte)タグの長さ
(nbyte)上記タグの長さ分のデータ

が入ります。タグは複数ありますが、タグ数などは記述されてなくて、ファイルの終端に来たら終わりと言う事っぽいです。
ここで、waveファイルで重要なタグは'fmt 'と'data'です。
通常'fmt 'は'WAVE'と連続して、その後に'data'が入りますね。
拡張して別のタグを入れて歌詞や曲の情報を入れるフォーマットもあるみたいですが、とりあえず再生する音だけに関しては'fmt 'と'WAVE'で十分です。

'fmt 'チャンクには、APIなどにそのまま放りこめるPCMWAVEFORMAT構造体が入っています。
非圧縮の普通のWAVファイルであれば、以下の16バイトでしょう。

Public Type WAVEFORMAT
	wFormatTag As Integer   - 形式。通常はWAVE_FORMAT_PCM=1
	nChannels As Integer    - チャンネル数。1=モノラル 2=ステレオ
	nSamplesPerSec As Long  - サンプリング数。8000,11025,22050,44100あたりが普通
	nAvgBytesPerSec As Long - 1秒あたりのバイト数。nBlockAlign*nSamplesPerSec。
	nBlockAlign As Integer  - 1ブロックあたりのバイト数。nChannels*wBitsPerSample/8
End Type

Public Type PCMWAVEFORMAT
	WF As WAVEFORMAT
	wBitsPerSample As Integer - サンプリンビット。8か16が普通
End Type
ただ、MP3のような生PCMデータではない場合、この構造体をより長くしたものが使われます。 通常PCMWAVEFORMATの後に、 (2byte)拡張部分のサイズ (nbyte)拡張部分 が入ります。 MP3の場合にはMPEGLAYER3WAVEFORMATが使用されます。
Public Type WAVEFORMATEX
  wFormatTag As Integer
  nChannels As Integer
  nSamplesPerSec As Long
  nAvgBytesPerSec As Long
  nBlockAlign As Integer
  wBitsPerSample As Integer
  cbSize As Integer
End Type
Public Type MPEGLAYER3WAVEFORMAT
  wfx As WAVEFORMATEX
  wID As Integer
  fdwFlags As Long
  nBlockSize As Integer
  nFramesPerBlock As Integer
  nCodecDelay As Integer
End Type

PCMWAVEFORMATではwBitsPerSampleまででしたが、ちょうど拡張部分のサイズがcbSizeにはいり、残りのデータがMPEGLAYER3WAVEFORMATのwID以降に入ります。
実際に身近なWAV形式MP3を見ると、'fmt 'チャンクのサイズは0x1Eとなっており、MPEGLAYER3WAVEFORMAT構造体のサイズと一致します。
acmStreamXxxxを使う場合は必要に応じて拡張部分を含むWAVEFORMAT(つまりMP3の場合ならMPEGLAYER3WAVEFORMAT)が必要なのですが、これがWAVファイルそのものから得られるのでラクです。
あとは、'data'チャンクの内容がちょうど変換すべきデータにあたるのでごっそりバッファかなんかに読みこんでおきましょう。
(別にちょびちょびファイルから読みこんでもいいけどさ)

で。
メンドイのがMP3ファイル。
MPEGLAYER3WAVEFORMAT構造体に相当するものをファイルからパパっと取り出せればいいのですが、そうもいかないようです。
そもそもMP3ファイルは、フレーム単位のデータが繋がっているだけで、複数のフレーム全体を取り囲むようなヘッダやフッタは基本的には存在しません。
そこで、個々のフレームデータからMPEGLAYER3WAVEFORMATを取得する事を考えます。
(VBR(可変ビットレート)については未調査。)

ただしその前に。
MP3ファイルにはID3v1、ID3v2のような曲名やジャンルなどを入力する項目があります。
これは少なくとも再生のことだけを考えれば全く邪魔なのであらかじめ取り除くことを考えます。
ID3v1及びID3v2の細かい内容は現在主流っぽいMP3タグ情報フォーマットまとめなどに掲載されているので、具体的にこの部分を書き換えたいとか思っている方は参照するといいでしょう。
ここではとりあえず取っ払う方法だけ考えます。

ID3v1は非常に簡単です。
ID3v1はMP3ファイルの最後尾128byteとサイズが決まっています。
そのため、後ろから128byteの位置のデータが'TAG'であればID3v1が存在するとして128byte削って終了です。

ID3v1は128byte固定で文字列フィールドも最長30byteと長い曲名などを格納できないため、ID3v2が考えられたようです。
ID3v2はファイルの先頭にあるため、ファイルの先頭3byteが'ID3'であればID3v2タグがあると考えられます。
ID3v2にはバージョンが存在し、これを書いてる時点ではv2.4.0まで来ているようです。
まぁ下位互換性はあると思われるのでとりあえずこれをサポートすれば現状ではOKでしょう。
(しかし「考えられる」「思われる」「とりあえず」って適当だなぁ・・・)

ID3v2の構造は先ほどのリンク先からたどればわかりますが、最初の10バイトがヘッダになっています。
そのうち最後の4バイトにID3v2全体のサイズからヘッダの10byteをひいた物が格納されています。
ただし、MidiのSMF形式のように、最上位ビットは使用せず、各バイトに対し残り7bitを用いた計28bitのデータが実際に利用されます。
また、ヘッダの6バイト目にはいくつかのフラグがありますが、下から5番目のビットが1である場合、フッタが存在するとしてもう10バイト長いタグと計算されます。

作成したプログラムでは以下のような計算でID3v2の長さを計算しています。
	Index = 10
	If ID3Header(5) And &H10 Then Index = 20

	'ヘッダ全長
	Index = Index + 2& ^ 21 * ID3Header(6) + 2& ^ 14 * ID3Header(7) _
			+ 2& ^ 7 * ID3Header(8) + ID3Header(9)

これでID3v1もID3v2も取っ払い、生のウェーブデータの圧縮部が取れました。
この圧縮部はWAV形式のMP3でもMP3ファイルでも同じです。
ただ、WAV形式の場合と違いMP3ファイルに関してはフレームのヘッダからMPEGLAYER3WAVEFORMATを設定する必要があります。

次回はMP3のフレームデータからMPEGLAYER3WAVEFORMATを設定する方法について触れ、実際にデコードを行う予定です。

03/03/14 簡易OSを作ろう編−3(ブートセクタの解析)[★★★◎]
と、いうわけで、FDのブートセクタを逆アセンブル・解析してみました。
って解析したのは去年の7月だったんだけど・・・更新ネタがちょっと尽きてきたかな・・・


んで。
ぱっとみてわかるのは、86系の比較的初期の命令しかつかって無いと言うところです。
掛け算すらないし、そもそも16bitコードです。
基本的に除算もないのですが、なぜか1箇所だけあったり。
余りが6バイトしかないからなぁ・・・

起動時はFDの先頭セクタ512バイトを0000:7C00-7DFFに読みこみ、7C00からスタートします。
全体の目的は、ルートディレクトリ中のIO.SYSというファイルを読みこみ、実行する事です。


最初に512バイトのおおよその概要を書いておきます。
0000 - 003D BPB部分
003E - 0045 スタックポインタ初期化
0046 - 0082 FDドライブのパラメータ保存、FDドライブ初期化 → 終了後00A3へ飛ぶ

0083 - 008B エラー時処理:"I/O error."を表示して009Eの再起動ルーチンへ
008C - 0092 エラー時処理:"No system."を表示して続く再起動ルーチンへ
0093 - 00AD 再起動処理:"Hit any key."を表示して再起動

00AE - 00DC FAT領域の読み込み
00DD - 00F5 ルートディレクトリからIO.SYSのあるクラスタを探索
00F6 - 012F クラスタ番号からセクタ番号への変換
0130 - 0169 IO.SYSのうち3セクタを読み込み、そこへジャンプ → あとはIO.SYSの制御へ

以下はサブルーチン
016A - 0177 [DS:SI]の文字列を表示
0179 - 0183 [ES:BX]にdl(7C24hの中身)から1セクタ読みこみ
0184 - 01B9 CHS形式−[7C50]/[7C52]/[7C54]の値を調整する
01BA - 01C6 CHS形式−[7C50]/[7C52]/[7C54]の値をint 13hで利用するレジスタに読みこみ

01C7 - 01D1 文字列:"IO      SYS"
01D2 - 01DE 文字列:"\nNo System."
01DF - 01EB 文字列:"\nI/O error."
01EC - 01F9 文字列:"Hit any key."
01FA - 01FD 未使用4バイト
01FE - 01FF ブートセクタであることを示す55AAの定数

メモリの使い方。
間にOSが入っているわけでもないので、通りすぎたコード領域をそのまま変数用メモリとして使っているようですね。
0078 -- 後で7C42を代入する
007A -- 後で0000を代入する

7C3E -- 起動時の0078hの中身を保存
7C40 -- 起動時の007Ahの中身を保存

7C42--7C4D -- 0078hの示すアドレスから11byte分コピーする
7C44 -- トラックあたりヘッダ数
7C49 -- 0Fh
7C4C -- 予約セクタ+FAT数*FATセクタ(==ルート)
7C4E -- 予約セクタ+FAT数*FATセクタ
7C50 -- 0
7C52 -- 0
7C54 -- 1

開始からFDドライブの初期化まで

で、こっからがコード。
左側の
:0000
は実際には7C00だけずれてます。

:0000 EB3C                   jmp 003E
:0002 90                     nop
:0003 2A 5F 2D 4B 3E 49 48 43 00 02 01 01 00 02 E0 00
      40 0B F0 09 00 12 00 02 00 00 00 00 00 00 00 00
      00 00 00 29 00 00 00 00 4E 4F 20 4E 41 4D 45 20
      20 20 20 46 41 54 31 32 20 20 20 //BPB領域

ここまでは前回の戯れ言で出てきたBPBデータ。
最初のjmp命令でデータ部は読み飛ばされます。
以後変数の処理が続くようですね。

:003E FA             cli             //割りこみ不可
:003F 33C0           xor ax, ax      //ax=0
:0041 8EC0           mov es, ax      //es=ax
:0043 8ED0           mov ss, ax      //ss=ax
:0045 8D26007C       lea sp, [7C00]  //sp=7c00h

スタックポインタを初期化しています。
:0049 BF7800         mov di, 0078    //di=78h
:004C 26C535         lds si, es:[di] //si=*(0078h) ds=*(007Ah)
:004F 89363E7C       mov [7C3E], si  //*(7C3Eh) = si
:0053 8C1E407C       mov [7C40], ds  //*(7C40h) = ds

:0057 FC             cld             //ループフラグクリア DF=0 inc
:0058 BF427C         mov di, 7C42    //di=7C42h
:005B B90B00         mov cx, 000B    //cx=0Bh
:005E F3             repz            //cx=0の間リピート
:005F A4             movsb           //[DS:SI]→[ES:DI] 11バイト

0078や007Aの内容を一端7C3Eと7C40に退避させていますね。
SIレジスタは0078の内容が入っているので、そこから11バイトDI=7C42にコピー。

:0060 8ED8           mov ds, ax      //ds=ax
:0062 C606497C0F     mov byte ptr [7C49], 0F //*(7C49h)=0Fh
:0067 8A0E187C       mov cl , [7C18]         //cl=*(7C18h) トラックあたりヘッダ数
:006B 880E447C       mov [7C44], cl          //*(7C44h)=cl
:006F C7067800427C   mov word ptr [0078], 7C42 //*(0078h)=7C42h
:0075 A37A00         mov word ptr [007A], ax   //*(007Ah)=ax
:0078 FB             sti             //割りこみ許可

ax=0なので最初でds=0、7C49には0Fh=15を代入していますね。
コメントにあるように7C18には1つのトラック当たりのセクタ数が入っていますが、それを7C44にコピーしています。
0078、007Aにはなんか値をいれていますが、なぜ7C42とaxなのかはわかりませんでした・・・

(2005/12補足)---------------------------------
0078の番地には、割り込み番号0x1Eに対応する割り込みハンドラのアドレスが入っているはずです。
しかし、割り込みリストを見ると、0x1EはFDドライブのパラメータリストが入る領域を指しているようです。
通常0078:007AはF000:EFC7を指しており、この領域はBIOS内にあります。
このパラメータは通常11byteで出来ています。

上のプログラムではその中身11byteを一端7C42にコピーし、0078に7C42を、007Aにax(=0000)を代入しています。
これが意味するところは、
「起動時のBIOSが持つFDドライブパラメータを一端メモリ中にコピーしておき、以後FDのアクセス時にはそちらを参照させる」
と言うことです。
(2005/12補足ここまで)---------------------------------


:0079 B400           mov ah, 00      //ah=0
:007B 32D2           xor dl, dl      //dl=0
:007D CD13           int 13          //int 13h(Disk Systemリセット)
:007F 7202           jb 0083         //0083h エラー時"I/O error."表示し停止
:0081 EB2B           jmp 00AE        //00AEh 成功時ジャンプ

Interrupt Jump Tableなどをみるとわかりますが、ah=0でのint 13hの割りこみは「RESET DISK SYSTEM」だそうです。
FDドライブの初期化かな?
失敗するとCFフラグが立つので、jbで以後0083以降のエラー処理に飛びます。
成功時はエラー処理を通過して00AEまでジャンプします。


エラー処理&再起動

//エラー時
    // I/Oエラー時

    * Referenced by a (U)nconditional or (C)onditional Jump at Addresses:
    |:007F(C), :00DB(C), :0142(U)
    |
    :0083 8D36DF7D     lea si, [7DDF] //si=7DDFh "I/O error."
    :0087 E8E000       call 016A      //016Ah呼び出し(上記文字列表示)
    :008A EB07         jmp 0093       //0093hジャンプ

上から飛んできた場合のエラー処理ルーチンですね。
7DDFには文字列「I/O error.」が入っています。
そのまま文字列表示関数の入っている016Aを呼び出します。
(JmpとちがいCallは関数呼び出しなのであとで戻ってくる。)
016Aはsiの差すアドレスの文字列を表示する関数ですね。

それが終わると再起動ルーチン0093へ飛びます。

次のプログラムはまた別のところから呼ばれるサブルーチンです。

    // OS見つからず

    * Referenced by a (U)nconditional or (C)onditional Jump at Address:
    |:00F4(C)
    |
    :008C 8D36D27D     lea si, [7DD2] //si=7DD2h "No system."
    :0090 E8D700       call 016A      //016Ah呼び出し(上記文字列表示)

これは後でIO.SYSファイルが見つからない時のエラーですね。
7DD2には「No system.」という文字列が入っています。
やはり文字列表示をした後そのまま再起動ルーチンに続きます。

    //エラー・OS見つからず・・・

    * Referenced by a (U)nconditional or (C)onditional Jump at Address:
    |:008A(U)
    |
    :0093 8D36EC7D     lea si, [7DEC]          //si=7DECh "Hit any key."
    :0097 E8D000       call 016A               //016Ah呼び出し(上記文字列表示)
    :009A 33C0         xor ax, ax              //ax=0
    :009C CD16         int 16                  //キー入力待ち

7DECの「Hit any key.」をやはり016Aで表示した後、ax=0でint 16hを呼んでいます。
これはキーボード入力用の命令ですが、キーが押されるまで制御が戻らないようですね。

    :009E BF7800       mov di, 0078            //di=78h
    :00A1 A13E7C       mov ax, word ptr [7C3E] //ax=*(7C3E)        7C3Eから4バイトを
    :00A4 8905         mov [di], ax            //*(di)=*(78h)=ax   78hの指すアドレスに
    :00A6 A1407C       mov ax, word ptr [7C40] //ax=*(7C40)        出力(si,ds)
    :00A9 894502       mov [di+02], ax         //*(di+2)=*(80h)=ax 起動時に戻す
    :00AC CD19         int 19                  //リブート?

再起動ルーチンです。
:004F 89363E7C
:0053 8C1E407C
で7C3E、7C40に0078、0080の値を退避させていたため、その値を戻してint 19hを呼びます。
int 19hは「BOOTSTRAP LOADER」でブートレコードを実行するので、BIOSで設定されている次のブート用ディスクの処理に入ります。
このFDからのブートは諦めて次へ行こうと言うことですね。


FATからIO.SYSを探す

ようやくエラー処理ルーチンを抜け、int 13hが成功した時の処理です。

//成功時

* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0081(U)
|
//各種変数初期化
:00AE A10E7C         mov ax, word ptr [7C0E] //ax=*(7C0E) 予約セクタ数
:00B1 32ED           xor ch, ch              //ch=0
:00B3 8A0E107C       mov cl, [7C10]          //cl=*(7C10h) FAT数
:00B7 0306167C       add ax, [7C16]          //ax+=FATセクタ数
:00BB E2FA           loop 00B7               //上をFAT数繰り返すことでax=予約セクタ+FAT数*FATセクタ

上記は予約セクタ数+FAT数*FATセクタを求めています。
乗算を使わず、あえてループして足し算をしているのは古いCPUの名残でしょうか?
予約セクタ数とFATセクタの次はルートディレクトリの内容が入っているはずですね。

:00BD A34C7C         mov word ptr [7C4C], ax //*(7C4Ch) = ax
:00C0 A34E7C         mov word ptr [7C4E], ax //*(7C4Eh) = ax
:00C3 33C0           xor ax, ax              //ax=0
:00C5 A3507C         mov word ptr [7C50], ax //*(7C50h) = ax = 0
:00C8 A3527C         mov word ptr [7C52], ax //*(7C52h) = ax = 0
:00CB 40             inc ax                  //ax++ (0->1)
:00CC A3547C         mov word ptr [7C54], ax //*(7C54h) = ax = 1

ここは先ほどの「予約セクタ数+FAT数*FATセクタ」他0や1をメモリに代入していますね。

:00CF E8B200         call 0184               //0184h CHS設定
:00D2 E8E500         call 01BA               //01BAh CHS読みこみ

Cylinder,Head,Sectorの値を設定するルーチンです。
0184は適切に割り算などして、シリンダ・ヘッド・セクタ番号を範囲内に納めるルーチンです。
01BAはその値をセクタ読みこみルーチンint 13hに合うようにcxなどのレジスタに値を格納するルーチンです。

:00D5 BB0005         mov bx, 0500            //bx=500h 読みこみセクタ書きこみ先
:00D8 E89E00         call 0179               //0179h 1セクタ読みこみ
:00DB 72A6           jb 0083                 //失敗したらI/O error.

読みこんだセクタの書きこみ先を0500として、1セクタの内容を読みこむ関数0179を呼んでいます。
失敗すると先ほど出てきた「I/O error.」を表示して再起動するルーチンに進むようです。

:00DD BF0005         mov di, 0500            //di=500h
:00E0 F6450B08       test byte ptr [di+0B], 08 //*(di+0Bh) の下から4bit目チェック(ボリュームラベル?)
:00E4 7403           je 00E9                 //立ってなければ00E9へ
:00E6 83C720         add di, 0020            //di+=0020 ボリュームラベルなら次のレコードへ

で、ルートディレクトリの先頭レコードをチェックします。
先頭レコードがボリュームラベルだったら1レコード=32byteだけポインタを進めます。

* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:00E4(C)
|
:00E9 57             push di        //di保存
:00EA 8D36C77D       lea si, [7DC7] //si=7DC7 "IO.SYS" --- DI=ID.SYS?
:00EE B90B00         mov cx, 000B   //cx=000B
:00F1 F3             repz           //
:00F2 A6             cmpsb          //cx=11bitが等しいか?----------
:00F3 5F             pop di         //diとりだし
:00F4 7596           jne 008C       //IO.SYS探索失敗→"No system." 008Ch

diにはルートディレクトリの先頭レコード(ボリュームラベルがあればその次)が入っていることになります。
diを使うので一端pushしておき、先頭レコードにあるファイル名がIO.SYSであるかどうかチェックします。
7DC7には「ID______SYS」と6つスペースが入っていますが、元々FAT12,16では8-3形式に合わせてスペースが充填されるのでこれで正しいです。
repz-cmpsbはcx分だけ連続するメモリ領域が等しいかどうかをチェックします。
IO.SYSがない場合は008Cの「No system.」の表示+再起動に飛びます。
これをみると、IO.SYSがあるだけではだめで、先頭レコードにある必要があるようですね。

:00F6 268B451A       mov ax, es:[di+1A] //ax=*(di+1A) 先頭クラスタ下位2バイト
:00FA 48             dec ax  //
:00FB 48             dec ax  //ax -= 2

先頭クラスタがIO.SYSであることがわかったので、実際に読みこみます。
まずIO.SYSの実際のデータが入っているクラスタからセクタを求めます。
各レコードの1Ahバイト目にクラスタ番号が入っています。
前回書いたように、クラスタ番号は002から始まるため、2を引きます。

:00FC 33DB           xor bx, bx //bx=0
:00FE 32ED           xor ch, ch //ch=0
:0100 8A0E0D7C       mov cl , [7C0D] //cl=*(7C0Dh) セクタ/クラスタ
:0104 03D8           add bx, ax //bx += ax
:0106 E2FC           loop 0104 //bx = ax*cl 何セクタ目か?

クラスタ当たりのセクタ数を取得します。
まぁ普通のFDDなら1なんですが。
その後、clの回数axの足し算をする事でbx=ax*clが入ります。
クラスタ番号*(セクタ/クラスタ)なので、bxは結局セクタ番号が入ります。

:0108 011E4C7C       add [7C4C], bx //*(7C4Ch) += bx ルート位置+セクタ
:010C 891E4E7C       mov [7C4E], bx //*(7C4Eh) = bx  セクタ

先ほどのセクタ番号は0からスタートするものなので、実際にはBPB、FAT、ルートディレクトリの分ずらす必要があります。
:00BD A34C7C
で7C4Cにはルートディレクトリのセクタ番号が入っているので、それに足しこみます。
7C4Eには

:0110 8B1E0B7C       mov bx, [7C0B] //bx=*(7C0Bh) バイト/セクタ(512)
:0114 B105           mov cl, 05 //cl=05h
:0116 D3EB           shr bx, cl //bx>>cl(bx>>5) (512/32=16)1セクタ中ファイルエントリ

最後にルートエントリ分を足す必要があります。
bxにセクタ当たりのバイト数(512)を読みこみ、右に5ビットシフトしています。
1レコードが32バイトなので、1セクタ当たりのレコード数を求めるんですね。

:0118 A1117C         mov ax, word ptr [7C11] //ax=*(7C11h) ルートエントリ最大数
:011B F6F3           div bl //ax/bl ルートエントリは何セクタ?
:011D 0AE4           or ah, ah //ah==0 
:011F 7402           je 0123 //ax mod bl=0 then 0123h
:0121 FEC0           inc al //al++ セクタ数余りなら繰り上げ

1セクタ当たりのレコード数が求まったので、次にルートディレクトリのセクタ数を求めます。
axにルートレコードのレコード数が求まるので、それを1セクタ当たりのレコード数で割ります。
割り切れない場合、div blの余りがahに入るので、ahが0かどうかをチェックし、0でなければ
商alを繰り上げます。
これでalにルートディレクトリのセクタ数が入ったことになります。

* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:011F(C)
|
:0123 32E4           xor ah, ah //ah=0
:0125 01064C7C       add [7C4C], ax //*(7C4Ch) += ax ルートエントリ分追加
:0129 01064E7C       add [7C4E], ax //*(7C4Eh) += ax ルートエントリ分追加
:012D E85400         call 0184 //現在ルートエントリを指すCHSにルート分追加
:0130 B90300         mov cx, 0003 //cx=3 
:0133 BB0007         mov bx, 0700 //bx=700h

ah=0は前のdiv blで値が変更されてしまったのを戻すためでしょう。
ルートエントリ分を7C4C,7C4Eに足し、0184を呼び出して適切なCHSを設定します。
bx=0700はIO.SYSの読みこみ先です。

//3セクタI/O sysから呼び出し

* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0155(U)
|
:0136 51             push cx //cx,bxの保存
:0137 53             push bx //
:0138 E87F00         call 01BA //7C50,7C52,7C54の値を読みこむ
:013B E83B00         call 0179 //1セクタbxに読みこみ
:013E 5B             pop bx  //bx=700hに復帰
:013F 59             pop cx  //cx=3に復帰
:0140 7303           jnb 0145 //エラーなし
:0142 E93EFF         jmp 0083 //エラー時0083h

01BAと0179でIO.SYSの内容をbx=700へ読みこみます。
エラーがなければ0145へ続きますが、エラーがあると0083の「I/O error.」表示+再起動へ飛びます。

* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0140(C)
|
:0145 49             dec cx         //cx--
:0146 740F           je 0157        //cx==0 0157h
:0148 C7064E7C0100   mov word ptr [7C4E], 0001 //*(7C4E)=1
:014E E83300         call 0184      //0184 (1セクタずつすすめる)
:0151 031E0B7C       add bx, [7C0B] //bx+=*(7C0Bh) (512バイト/セクタ)
:0155 EBDF           jmp 0136       //0136h

ここに入る段階ではcx=3なのでいきなりje 0157にはひっかかりませんね。
セクタ番号を1進め、読みこみ先を512バイト進めていることから、IO.SYSを3セクタ分読みこみたいようです。

* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0146(C)
|
:0157 8A2E157C       mov ch, [7C15] //ch=*(7C15)  //メディアタイプディスクリプタ
:015B 8A16247C       mov dl, [7C24] //dl=*(7C24h) //ドライブ番号
:015F 8B1E4C7C       mov bx, [7C4C] //bx=*(7C4C)  //IO.SYSの実行部セクタ番号
:0163 33C0           xor ax, ax     //ax=0
:0165 EA00007000     jmp 0070:0000  //IO.SYSの来るアドレス

あとはIO.SYSのアドレスへ制御を移します。
必要なデータをレジスタに入れ、IO.SYSに飛びます。
(2005/12修正) IO.SYSは0700に書き込んだので、そこからプログラムの実行を行うため、0070:0000にジャンプします。

これで全体の流れは終了です。


あとはまた小さな関数がいくつか入っています。

//[DS:SI]以下の文字列をNULL文字に至るまで表示

    * Referenced by a CALL at Addresses:
    |:0087, :0090, :0097
    |

    * Referenced by a (U)nconditional or (C)onditional Jump at Address:
    |:0177(U)
    |
    :016A AC           lodsb        //al=[DS:SI] , SI++
    :016B 0AC0         or al , al   //al != 0 ?
    :016D 7501         jne 0170     //al>0なら0170
    :016F C3           ret          //それ以外はリターン

これは文字列表示の関数です。
SIに表示したい文字列のアドレスが渡されますね。
lodsbは[DS:SI]の中身をalに読みこみつつ、SIをインクリメントする命令です。
al=*(si++)と思えばいいでしょうか。
読みこんだ値が0であればorの値が0になりますが、それ以外の場合は1文字表示するルーチンの0170に飛びます。
0まで来たら関数を抜けます。

    * Referenced by a (U)nconditional or (C)onditional Jump at Address:
    |:016D(C)
    |
    :0170 BB0700       mov bx, 0007 //bx=0007h
    :0173 B40E         mov ah, 0E   //ah=0Eh
    :0175 CD10         int 10       //alの文字を白色で出力
    :0177 EBF1         jmp 016A     //016Ahへ戻る

上から続く1文字表示の関数です。int 10hは画面制御の命令が色々入っています。
ah=0Eの時は「TELETYPE OUTPUT」が呼ばれます。
blで表示の色を指定できますが、ここでは07で白色ですね。
VBでいうQBColor関数と同じ色指定の仕方と同じです。
1文字表示すると再び1文字読みこみに016Aへ戻ります。

//1セクタ読みこみルーチン

    * Referenced by a CALL at Addresses:
    |:00D8, :013B
    |
    :0179 B402         mov ah, 02     //ah=02
    :017B B001         mov al, 01     //al=01
    :017D 8A16247C     mov dl, [7C24] //dl=*(7C24)
    :0181 CD13         int 13         //[ES:BX]にdlから1セクタ読みこみ
    :0183 C3           ret

7C24の差すセクタ番号から1セクタ読みこみ、bxのさすアドレスに格納します。
ah=02の時のint 13hは「READ SECTOR(S) INTO MEMORY」で名前の通りです。
読みこむセクタ数はal=1、ドライブ番号は7C24になります。
関数01BAでシリンダ・ヘッド・セクタ番号(CHS)は既に各レジスタに

//チェックルーチン?
    //[7C50] -- ヘッダ番号
    //[7C52] -- トラック番号
    //[7C54]+[7C4E] -- セクタ番号
    //ヘッダ・トラック・セクタ番号を所定の範囲内の数値に押さえる
    //[7C50]/[7C52]/[7C54]に書きこむ

    * Referenced by a CALL at Addresses:
    |:00CF, :012D, :014E
    |
    :0184 8B3E507C     mov di, [7C50]   //di=*(7C50h)
    :0188 8B36527C     mov si, [7C52]   //si=*(7C52h)
    :018C A1547C       mov ax, word ptr [7C54] //ax=*(7C54h)
    :018F 03064E7C     add ax, [7C4E]   //ax += *(7C4E)
    :0193 EB05         jmp 019A         

    //ax=ルート位置+1
    //di=si=0

適切なCHS値を設定するルーチンです。
全体のセクタをリニアに扱いたいところですが、int 13hではCHSの3値でセクタを扱う必要があります。
そこで、割り算などをして範囲内に納めます。
元々C=[7C52]、H=[7C50]、S=[7C54]に納まっている値に[7C4E]セクタだけ進めた値を取得します。
si=C、di=H、ax=Sが入っている状態になります。
その後019A以降の割り算ルーチンへ飛びます。

    * Referenced by a (U)nconditional or (C)onditional Jump at Address:
    |:019E(C)
    |
    :0195 2B06187C     sub ax, [7C18]   //ax -= *(7C18h)
    :0199 46           inc si           //si++

    * Referenced by a (U)nconditional or (C)onditional Jump at Address:
    |:0193(U)
    |
    :019A 3B06187C     cmp ax, [7C18]   //ax <> *(7C18h) (トラックあたりセクタ数)
    :019E 77F5         ja 0195          //ax > *(7C18h)なら 0195h
    
    //si=セクタ番号/トラックあたりセクタ数 = トラック番号
    //ax=セクタ番号modトラックあたりセクタ数 = トラック内セクタ

上からは019Aに飛んできますね。
S=axが[7C18]=トラック当たりセクタ数より大きい限り0195に飛びます。
0195ではaxをトラック当たりセクタ数引いてその分トラック数を1増やしてます。
要はトラック当たりセクタ数より大きい分だけヘッダ番号に繰り上げをしてるんですね。

    :01A0 A3547C       mov word ptr [7C54], ax //*(7C54) = ax
    :01A3 8BC6         mov ax, si       //ax = si
    :01A5 EB05         jmp 01AC         //01ACh

この時点ではセクタ番号は確定なので[7C54]に戻しています。


    //[7C54]にトラック内セクタ番号
    //ax=トラック番号

    * Referenced by a (U)nconditional or (C)onditional Jump at Address:
    |:01B0(C)
    |
    :01A7 2B061A7C     sub ax, [7C1A]  //ax -= *(7C1A) (ドライブヘッド数)
    :01AB 47           inc di          //di++
    
    
    * Referenced by a (U)nconditional or (C)onditional Jump at Address:
    |:01A5(U)
    |
    :01AC 3B061A7C     cmp ax, [7C1A] //ax <> *(7C1A) (ドライブヘッド数)
    :01B0 73F5         jnb 01A7       //ax>=*(7C1A)なら01A7hへ
    
    //di=トラック番号/ヘッドあたりドライブ数 = ヘッダ番号
    //ax=トラック番号modヘッドあたりドライブ数 = ヘッダ内トラック番号

今度は同様にヘッドの値を繰上げしてシリンダの値を増やしています。

    :01B2 A3527C       mov word ptr [7C52], ax //*(7C52) = ax
    :01B5 893E507C     mov [7C50], di //*(7C50) = di
    :01B9 C3           ret

終わったら[7C52]と[7C50]を設定し、関数を抜けます。
ここで設定した値は後で01BAで呼び出されます。


//7C50,7C52,7C54の値を読みこむ(int13h呼び出し用)
    //[7C50]/[7C52]/[7C54] = ch/dh/cl

    * Referenced by a CALL at Addresses:
    |:00D2, :0138
    |
    :01BA 8A2E507C     mov ch, [7C50] //ch=*(7C50)
    :01BE 8A36527C     mov dh, [7C52] //dh=*(7C52)
    :01C2 8A0E547C     mov cl, [7C54] //cl=*(7C54)
    :01C6 C3           ret

シリンダ・ヘッド・セクタ番号を適切にレジスタにセットするルーチンです。
この関数を読んだ後0179が呼ばれますね。

:01C7 49 4F 20 20 20 20 20 20 53 59 53       "ID      SYS"
:01D2 0D 0A 4E 6F 20 73 79 73 74 65 6D 2E 00 "\nNo System.\0"
:01DF 0D 0A 49 2F 4F 20 65 72 72 6F 72 2E 00 "\nI/O error.\0"
:01EC 20 48 69 74 20 61 6E 79 20 6B 65 79 2E 00 "Hit any key.\0"
:01FA 00 00 00 00
:01FE 55             push bp
:01FF AA             stosb

あとは文字列データ。最後の55AAは何だかわかりませんが、残りはたったの6バイト!
これはあんまり凝った事はできないな・・・
以上で終了です。
HDDの場合は、パーティションテーブルを読みこんで、アクティブなパーティションの先頭セクタに制御を移すコードが入ります。

色々やってますが、結局IO.SYSを読みこむだけ(^^;)
次回はようやくアセンブラでコードを書こうかと思います。

と言ってもファイルシステムがどうとかマルチスレッドがとか簡単にできるわけがないので、BIOSの命令を使って簡単なプログラムを作ってみたいと思います。
単なるプログラムではOSとは言えないので、intの割りこみ命令を定義する予定です。


一覧へ戻る