Ch 21 光碟機 (2)

在這一章堙A小木偶將要談談如何用組合語言播放音樂光碟 ( audio CD 也叫 CD-DA ),所謂音樂光碟就是遵循紅皮書所規範的光碟。我這樣講相信一堆人仍然搞不清楚,事實上音樂光碟就是能夠在 CD 唱盤上播放的光碟片啦,它的發明是為了取代唱片,市面上唱片行所賣的歌手的 CD 就是這一種光碟。早在電腦尚未流行之前的 1980 年,Philips 和 Sony 公司為了以光碟播放音樂而制定的規格,後來衍生出來的 Video CD 、CD-I、Data CD 等等都是以它為基礎,世界上所有的光碟機都可以播放此種光碟。在寫程式之前,應先對音樂光碟的構造有所了解。

音樂光碟構造

光碟片保存資料的原理和磁片不同,它並不是藉由磁性物質保存資料,光碟片上面有許多肉眼看不到的細小坑洞 (pit),組成一個螺旋型軌道由光碟片外向內延伸。當光碟機的讀寫頭發出的雷射光照到光碟片上的這些坑洞形成反射,比照射到非坑洞反射光行經更遠的路,於是產生相差,比較相差的變化就可形成不同的訊號。

一張標準的音樂光碟最多可以錄 74 分鐘的音樂,光碟上的每一首歌曲,都稱為一『音軌』(track)(註一),每一首歌曲之間還有兩秒鐘的空白 gap)。習慣上音樂光碟是以時間長度來表示大小的,像 MM:SS:FF 這樣,其中 MM 表示分鐘,SS 表示秒鐘,FF 表示『格』(frame,也有人稱為磁區(sector))。每 75 格形成一秒鐘,每 60 秒形成一分鐘。根據上述規則,您就可以計算第幾分第幾秒相當於絕對磁區多少了。舉例來說,如果您在 CD 唱盤上看到現在已經到 02:32,因為 CD 唱盤並沒有顯示第幾格,所以假設第零格,那現在就應該正在播放第

2*60*75 + 32*75 = 11400

磁區,您也可以計算一張標準的音樂唱片最多有

74*60*75 = 333000

個磁區了。

CMD=03/0A/AX=1510H/INT 2FH 取得 CD 資訊

這個服務程式可以得到音樂光碟的音軌數及結束磁區。呼叫前也要先設定 device driver request header 結構體,這時候該結構體的命令碼為 3,表示 IOCTL INPUT,可以想成由光碟機輸入資料到主機板中;然後在 0E 起始的雙字組設定傳送偏移位址與區段位址,表示此位址將要接收由光碟傳回的資訊,這也可看成是一個結構體;在第 12H 開始的一個字組設定光碟機傳來的資料長度,7,表示共 7 個位元組;然後在 device driver request header 結構體的第 0 個位元組設定整個 device driver request header 結構體長度為 13H 個位元組;最後在傳送位址所指的 7 個位元組的第零個位元組設定 0AH,這個 0AH 是要傳給光碟機的命令資料,意思是要光碟機傳回光碟片資訊。再將光碟機編號存入 CX,1510H 存入 AX,最後呼叫 INT 2FH 即可。

當呼叫完成返回時,如果成功,在 device driver request header 所設定的傳送位址會存入由光碟機傳來的音樂光碟片資訊結構體,其格式如下:

位址  大小(位元組)    意     義
-------------------------------------------------
 00        1        命令資料,0AH,這是呼叫時用的
 01        1        開始音軌編號,應該都是 01
 02        1        結束音軌編號
 03        4        結束磁區編號

CMD=03/B/AX=1510H/INT 2FH 取得音軌資料

取得音軌資料的方法和上述取得音樂光碟資料方法類似,所不同的是傳送位址所指定的結構體格式不同:

位址  大小(位元組)    意     義
-------------------------------------------------
 00        1        命令資料,0BH,這是呼叫時用的
 01        1        欲查詢音軌編號
 02        4        該音軌起始的位置,以紅皮書 MM:SS:FF 表示
 06        1        Track control information

CMD=84H/AX=1510H/INT 2FH 演奏音樂

呼叫這個服務程式前,也要先設定device driver request header,其格式和前面不太相同,格式如下:

位址  大小(位元組)    意     義
---------------------------------------------------
 00        1       device driver request header 長度,15H??
 01        1       subunit code
 02        1       84H,即演奏音樂的命令碼
 03        2       傳回狀態,參考第 21 章註二
 05        8       皆設為 0,保留
 0D        0       位址模式,0 表示 HSG 方式,1 表示紅皮書方式
 0E        4       演奏位置,依位址方式以 HSG 或紅皮書方式表示
 12        4       演奏時間,均以 HSG 方式表示

先說說第 0DH 個位元組的位址模式吧。假如設為零表示以 HSG 方式計算,這種方式是以磁區編號表示,由第零號到最大 332999 號,也是內定的方式。假如是 1 則表示是依照紅皮書的方式表示,它是以 MM:SS:FF 的方式表示,則最高位址的位元組並沒有使用到,設為零;第二高的表示分鐘,0 到 74 分;第三高的表示秒鐘,0 到 60 秒;最低位址的位元組表示格,0 到 74 格。兩者的換算方式是:

磁區=分*60*75 + 秒*75 + 格 - 150

而演奏位置就是視位址模式而定,可以用 HSG 或紅皮書方式來決定。但是演奏時間卻只能以 HSG 方式訂定。

CMD=85H/AX=1510H/INT 2FH 停止演奏

這個服務程式必須設定 device driver request header 中的前 13 個位元組,而且命令碼為 85H 即可,如下表:

位址  大小(位元組)    意     義
---------------------------------------------------
 00        1       device driver request header 長度,0DH
 01        1       subunit code
 02        1       85H,即停止演奏音樂的命令碼
 03        2       傳回狀態,參考第 21 章註二
 05        8       皆設為 0,保留

當 device driver request header 設定好之後,使 ES:BX 指向該表,CX 為光碟機編號,使 AX 設為 1510H,再呼叫 INT 2FH 即可。

CMD=88H/AX=1510H/INT 2FH 繼續演奏

這個功能是使經由停止演奏的光碟繼續演奏,也是只需設定 device driver request header 中的前 13 個位元組,如下表

位址  大小(位元組)    意     義
---------------------------------------------------
 00        1       device driver request header 長度,0DH
 01        1       subunit code
 02        1       88H,即停止演奏音樂的命令碼
 03        2       傳回狀態,參考第 21 章註二
 05        8       皆設為 0,保留

當 device driver request header 設定好之後,使 ES:BX 指向該表,CX 為光碟機編號,使 AX 設為 1510H,再呼叫 INT 2FH 即可。

音樂光碟演奏程式

底下小木偶撰寫了一個程式,它能顯示音樂光碟有幾首歌曲,然後由使用者輸入要聽的歌曲編號,程式能演奏出來。小木偶將它寫成 CD_PLAY.ASM 檔,這個程式不能轉換成 CP_PLAY.COM 檔,只能由 CD_PLAY.EXE 來執行。執行時在 DOS 模式下:

H:\HomePage\SOURCE>cd_play [Enter]
這片光碟共有 17 首曲子,您要聽第聽第幾首?03
H:\HomePage\SOURCE>

輸入 03 表示聽第三首歌。底下是原始碼列表:

rq      struc           ;位址---意-----義-------
len     db      ?       ; 00 device driver request header 長度
subunit db      ?       ; 01 subunit,0
command db      ?       ; 02 命令碼
status  dw      ?       ; 03 返回狀態
reverse dq      ?       ; 05 保留,0
address db      ?       ; 0D 媒體描述子
trn_off dw      ?       ; 0E 傳送資料位址,偏移位址
trn_seg dw      ?       ;    區段位址
len_tr  dw      ?       ; 12 傳送資料長度
rq      ends

rq1     struc           ;位址---意-----義-------
len1    db      ?       ; 00 device driver request header 長度
subuni1 db      ?       ; 01 subunit,0
comman1 db      ?       ; 02 命令碼
status1 dw      ?       ; 03 返回狀態
revers1 dq      ?       ; 05 保留,0
addres1 db      ?       ; 0D 位址模式
ply_sec dd      ?       ; 0E 開始演奏磁區
ply_tim dd      ?       ; 12 演奏時間
rq1     ends

        .286
;***************************************
cd_data segment
;下一行為存放每一音軌的起始磁區編號,每一音軌佔 4 個位元組
track   db      100 dup ( 4 dup(0) )    ;028 起始磁區資料

;訊息:
mes1    db      '沒有光碟機$'
mes2    db      '這片光碟共有 '
mes2_no db      '00 首曲子,您要聽第聽第幾首?$'

;得到光碟片資料所需的 device driver request header
disk_info       rq      <13h,0,3,?,0,0,offset dsk_ifo,seg cd_data,7>
dsk_ifo         db      0ah,?
total_tracks    db      ?       ;038 音軌總數
ending_sector   dd      ?

;得到音軌資料所需的 device driver request header
track_info      rq      <13h,0,3,?,0,0,offset trk_ifo,seg cd_data,7>
trk_ifo         db      0bh
track_no        db      1       ;044 音軌編號
track_start     dd      ?       ;045 該音軌之起始位置,MM:SS:FF
track_ctl       db      ?

;演奏音軌所需的 device driver request header
play            rq1     <15h,0,84h,?,0,0,?,?>

;底下是一些變數
cd_driver       dw      ?       ;052 CDROM機編號
cd_data ends
;***************************************
stack   segment stack           ;055 堆疊區段
        db      20 dup ('my stack')
stack   ends
;***************************************
code    segment
        assume  cs:code,ds:cd_data
;---------------------------------------
start:  push    ds              ;062 程式碼開始
        sub     ax,ax
        push    ax
        mov     ax,cd_data
        mov     ds,ax
        mov     es,ax

        sub     bx,bx
        mov     ax,1500h
        int     2fh             ;071 得到第一台CDROM機編號
        or      bx,bx
        jnz     play0
        mov     dx,offset mes1  ;074 沒有CDROM機
        mov     ah,9
        int     21h
exit:   mov     ax,4c01h        ;077 結束程式
        int     21h

play0:  mov     cd_driver,cx    ;080 得到光碟片資料
        mov     bx,offset disk_info
        mov     ax,1510h        ;082 在此程式中最重要的資訊是總歌曲數
        int     2fh             ;083 ,呼叫完後會存於 total_tracks

        mov     al,total_tracks ;085 將總歌曲數以 ASCII 碼的
        call    al2dec          ;086 形式存入 mes2 字串

;底下的程式目的是得到每一音軌的起始磁區編號
        mov     dl,0
        mov     di,offset track ;090 每一音軌的起始磁區編號存於 track 
play1:  mov     cx,cd_driver
        mov     bx,offset track_info
        mov     ax,1510h
        int     2fh             ;094 呼叫完後這一音軌的起始時間存於
        cld                     ;095 track_start 開始的 4 個位元組
;把起始時間轉換成磁區編號,磁區編號=分*60*75+秒*75+格-150,且為 32 位元
        mov     si,offset track_start
        lodsb                   ;098 AL=格
        cbw
        mov     bx,ax           ;100 BX=格
        mov     cl,75
        lodsb                   ;102 AL=秒
        mul     cl              ;103 AX=秒*75
        push    dx
        add     bx,ax           ;105 BX=秒*75+格
        mov     cx,60*75
        sub     bx,150          ;107 BX=秒*75+格-150
        lodsw
        sub     dx,dx           ;109 DX:AX=分
        mul     cx              ;110 DX:AX=60*75*分
        add     ax,bx
        adc     dx,0            ;112 DX:AX=60*75*分+秒*75+格-150
        mov     [di],ax         ;113 存入 track 陣列變數
        inc     di
        inc     di
        mov     [di],dx
        inc     di
        pop     dx
        inc     di
        inc     track_no
        inc     dl
        cmp     dl,total_tracks
        jnz     play1

        mov     dx,offset mes2  ;125 印出總音軌數
        mov     ah,9
        int     21h
play2:  call    key_in          ;128 輸入要聽的歌曲,並存於 BX
        cmp     bl,total_tracks ;129 檢查是否在總音軌數的範圍內
        jbe     play4
        mov     cx,2            ;131 不在範圍內,重新數入前游標退位兩格
play3:  mov     dl,08h
        mov     ah,2
        int     21h
        loop    play3
        jmp     play2
                                ;137 將要驗奏音軌及下一音軌的起始磁區存入
play4:  dec     bx              ;138 device driver request header 
        mov     di,offset play.ply_sec
        mov     cx,4
        shl     bx,2
        mov     si,offset track
        add     si,bx           ;143 取得要演奏音軌起始磁區位址
        rep     movsw

;要演奏音軌的磁區長度等於下一音軌的起始磁區減要演奏音軌起始磁區
        mov     di,offset play.ply_tim
        mov     si,offset play.ply_sec
        mov     ax,[di]
        mov     bx,2
        sub     ax,[si]
        mov     [di],ax
        mov     ax,[di+bx]
        sbb     ax,[si+bx]
        mov     [di+bx],ax
        mov     bx,offset play  ;156 演奏音軌
        mov     cx,cd_driver
        mov     ax,1510h
        int     2fh

        mov     ax,4c00h        ;161 結束程式
        int     21h
;---------------------------------------
;把總音軌數變成 ASCII 碼並存於 mes2_no 處
al2dec  proc    near
        cbw
        mov     cl,10
        mov     bx,offset mes2_no
        div     cl
        add     ax,3030h
        mov     [bx],ax
        ret
al2dec  endp
;---------------------------------------
;輸入要演奏的音軌編號,返回時 BX=要演奏的音軌編號
key_in  proc    near
        sub     bx,bx
        mov     cx,0a00h
key0:   mov     ah,0
        int     16h     ;181 鍵盤服務程式
        cmp     al,'0'
        jb      key0
        cmp     al,'9'
        ja      key0
        mov     dl,al
        mov     ah,2
        int     21h
        sub     al,'0'
        cbw
        xchg    ax,bx
        mul     ch      ;192 先輸入十位數故應把第一次輸入的數
        add     bx,ax   ;193 乘以 10,再加上現在輸入的個位數
        inc     cl
        cmp     cl,2
        jne     key0
        ret
key_in  endp
;---------------------------------------
code    ends
;***************************************
        end     start   ;202 原始程式結束

讓我來解釋這個程式吧。先說這個程式的主要架構,再說說細節部份。由小木偶所收集到的資料來看,如果要由程式播放音樂光碟上的其中一首曲子,沒有辦法直接指定要播放那一音軌,必須要找出該音軌的起始磁區或起始時間,然後再指定要播放幾個磁區,由上述資料去設定 device driver request header,呼叫 INT 2FH 中斷。因此小木偶設定一個 track 變數,作為存放每一音軌起始磁區編號,而播放磁區數就等於後面的音軌起始磁區編號減去前面音軌起始磁區編號,這樣大致架構就成了。

程式的前 21 行定義了兩個結構體,這是給 device driver request header 使用的,因為讀取音樂光碟資料和演奏音樂光碟所用的 device driver request header 不太相同,所以小木偶定義了兩個結構體。如果您用 MASM 6.11 來組譯,這兩個結構體的欄位名相同並不影響組譯結果,MASM 6.11 能自動分辨,但是如果您用 MASM 5.0 來組譯,則結構體欄位名要不相同才能正確組譯,顯然 MASM 6.11 是聰明得多了,但為了能用 MASM 5.0 組譯,所以小木偶取不同名稱的欄位。

.286 假指令

我們知道 80X86 家族的 CPU 都有向下相容的特性,也就是後一代的 CPU 可以執行前一代指令集,同時又更加強了或多了一些指令,在這個程式堙A小木偶在程式第 141 行用了一個 shl bx,2 指令,在 8086/8088 指令集並沒有這樣的指令,8086/8088 每次只能左移一個位元,如果要一兩個或兩個以上的話,必須把左移次數先放入 CL 堙A再左移。但是 80286 以上的 CPU 是可以寫成這樣的,假如沒有指定 .286 那組譯器會使用 8086/8088 指令集,故會產生錯誤,所以小木偶在程式開始指定使用 80286 的指令集。

程式第 28 行是定義一個 track 變數,這個變數事實上是一個很大的陣列,它以四個位元組為一單位,代表每個音軌的起始磁區編號,依據紅皮書所定義的音樂光碟,每片音樂光碟最大只能存放 99 個音軌,也就是 99 首歌曲,因此小木偶定義了 100 個音軌所需之起始磁區,共佔 400 個位元組。

程式第 32、33 行分開來寫的原因是為了使 163 到 172 行的 AL2DEC 副程式很方便的把音軌總數填入這個字串堙A然後一次由 AH=9H/INT 21H 印在螢幕上。第 35 到第 50 行共有三個結構體,分別是給取得音樂光碟資料、音軌資料、演奏音樂所需的 device driver request header 所用。其他的部份並不難了解,小木偶就不贅述了。

後記

我想這個程式還有很大的擴充空間,例如加上暫停、跳到上/下一首曲子、調整音量、開啟/關閉光碟機托盤、顯示正在彈奏的時間、停止播音等等,您都可以參考中山美麗之島精華區光碟驅動原始說明文件實作出來。


註一:光碟片上的『音軌』和軟碟片上的『磁軌』不同。光碟片上面並沒有像軟碟片一樣的同心圓軌道,而是一個螺形軌,所以只有一軌,標準的音樂光碟上有一個螺形軌道,上面共有 333000 個磁區(sector)

回到首頁到第二十章到第二十二章