1. 程式人生 > >組合語言(十四)彙編實現音樂的播放

組合語言(十四)彙編實現音樂的播放

原文:https://blog.csdn.net/xiaolanmyself/article/details/16927161 

        在介紹音樂的播放之前,先來說說如何用匯編髮出聲音,之後在介紹如何用發出有頻率的聲音。

        喇叭的構造大致如下圖所示,主要由紙盆、線圈、永久磁鐵等組成。當有電流通過線圈時,線圈產生的磁場和永久磁鐵的磁場相互作用,從而使線圈產生振動。和線圈相連的紙盆也隨之移動,若通過線圈的電流時連續變化的,則線圈移動的幅度也會變化,從而牽動紙盆振動,產生聲音。

        那麼PC機的小喇叭是怎樣與機器相連的呢?圖2-2表示了喇叭與機器簡單的相連情況。喇叭的一端連線在電源正極,另一端與機器的61H埠的bit位相連。可以想象,若能連續改變61H埠的bit位0,1狀態,就可以使喇叭線圈內的電流時有時無,從而使喇叭發聲。我們編制的彙編程式的工作,就是連續改變61H埠的bit位狀態。

        上面介紹了通過埠61H的bit位發出聲音的技術時遺留下一個問題,那就是如何精確的控制聲音的頻率。現在就解決這個問題。

        早期的PC機有一個專門用於定時的電路,型號為8253/8254。它有三個通道,第一個通道用於控制始終正常運轉;第二個通道用於儲存器重新整理;這兩個通道與我們現在要討論的問題無關。第三個就是一組電路域喇叭相連。

        如下圖所示就是PC機中完整的發聲電路,定時器通道3的G端與61H埠的bit0位相連,如果把61H埠的bit0位置為1,那麼定時器通道3就會被啟動,此時將有一組訊號從OUT輸出,訊號的頻率可以用程式控制;若61H的埠bit0位為0,則定時器被關閉,out端就恆為1.

發聲程式設計原理:

       PC機發聲系統以8254的2號計數器為核心。系統初始化時,2號計數器I作在“方波發生器”方式,初值為二進位制數,,寫入順序為先低後高,CLK2為1.193182MHz,當計數初值為533H時,OUT2輸出的方波大約為900Hz,經過簡單的濾波之後,送至揚聲器。改變1、4號計數器的計數初值就可以使揚聲器發出不同頻率的音響。

        ROM BIOS 中有個BEEP子程式,這能根據BL中組出的時間計數值控制8254定時器,產生持續時間為1個或幾個0.5秒,頻率為896HZ的聲音,我們可以利用並修改BEEP,使其產生任一頻率的聲音。為此我們需要做兩點修改,首先,BEEP程式只能產生896HZ的聲音,我們的通用發聲程式應能產生任一頻率的聲音。其次,BEEP產生聲音的持續時間(音長)只能是0.5秒的倍數,我們希望聲音的持續時間更易於調整,例如可以是10ms的倍數。

我們知道BEEP能將計數值533H送給定時器2產生896HZ的聲音的,那麼產生其它頻率聲音的時間計數值應為:

533H×896÷給定頻率=123280H÷給定頻率

發聲程式包括4個步驟: 

(1)在8253中的42埠送一個控制字0B6H(10110110B),該控制字對定時器2進行初始化,使定時器2準備接收計數初值。 

(2)在8253中的42H埠(Timer2)裝入一個16位的計數值(533H×895/頻率),以建立將要產生的聲音訊率。 

(3)把輸出埠61H的PB0、PB1兩位置1,發出聲音。 

        對於發音部分。PC機上的大多數輸入/輸出(I/O)都是由主機板上的8255(或8255A)可程式設計序外圍介面晶片(PPI)管理的。PPI包括3個8位暫存器,兩個用於輸入功能,一個用於輸出功能。輸入暫存器分配的I/O埠號為60H和62H,輸出暫存器分配的I/O埠號為61H。由PPI輸出暫存器中的0、1兩位來選擇揚聲器的驅動方式。

 (4)注意音樂節拍表的頻率表的設定。一個頻率對應一個節拍,如果頻率表和節拍表有問題,同樣不會發出聲音。

       對於發音部分。PC機上的大多數輸入/輸出(I/O)都是由主機板上的8255(或8255A)可程式設計序外圍介面晶片(PPI)管理的。PPI包括3個8位暫存器,兩個用於輸入功能,一個用於輸出功能。輸入暫存器分配的I/O埠號為60H和62H,輸出暫存器分配的I/O埠號為61H。由PPI輸出暫存器中的0、1兩位來選擇揚聲器的驅動方式。連線到揚聲器上的是定時器2,從上圖可以看到,GATE2與埠61H的PB0相連,當PB0=1時,GATE2獲得高電平,使定時器2可以在模式3(方波)下工作。定時器2的OUT2與埠61H的PB1通過一個與門與揚聲器的驅動電路相連。當PB1=1時,允許OUT2的輸出訊號到達揚聲器電路。因此,只有PB0和PB1同時為“1”時,才能驅動揚聲器地聲。通過以下指令實現: 

IN AL,61H 
OR AL,3 
OUT 61H,AL 

上面的指令用以開啟揚聲器,如要關閉揚聲器時則為: 

AND AL,0FCH 
OUT 61H,AL 

當從8255中採集到輸入的資料時,需要確定相應的頻率,所以在軟體程式設計時要建立一個數據表: 

TABLE DW 493,440,392,349,329,293,261 

把相應的頻率送到一個暫存器上,通過公式: 

計數值=533H×896÷ f=1234DCH÷ f 

算出計數值,再把算得的計數值送給8253,就可產生所要頻率的方波。在把計數值送8253前,必須先把8253進行初始化: 

MOV AL,0B6H 
OUT 43H,AL 

        使其選用通道2,工作在方式3下。 就整個電路而言,接好電路後,通過軟體程式設計不斷地採集從8255口中輸入的訊號,而8個開關都接在8255的A口上,只要有開關按下,就會採集到一個數據,根據這個資料與事先編好的表對應,得到一個計數值,把這個計數值送給8253的通道2,8253的通道2工作的方式3下,這樣就可以產生滿足頻率要求的發聲方波。這個方波經驅動放大就可以使揚聲器發出相應的聲音。 所以8255在這裡完成兩個任務,它不僅從A口中採集到資料,而且B口的PB1和PB0兩個位要控制發聲。8253的主要任務就是產生所要求發聲的不同頻率的方波。

;定義資料段
data segment
     infor1 db 0Dh, 0AH, "welocom you to come here listeng! $"

     mus_freg  dw 330,294,262,294,3 dup (330)     ;頻率表
               dw 3 dup (294),330,392,392
               dw 330,294,262,294,4 dup (330)
               dw 294,294,330,294,262,-1
     mus_time  dw 6 dup (25),50                   ;節拍表
               dw 2 dup (25,25,50)
               dw 12 dup (25),100
data ends

;棧段定義
stack segment stack
      db 200 dup(?)
stack ends

;--------字串輸出巨集----------
SHOWBM MACRO b
     LEA DX,b
     MOV AH,9
     INT 21H
 ENDM

;----------音樂地址巨集-----------
ADDRESS MACRO A,B
     LEA SI,A
     LEA BP,DS:B
ENDM
;-------------------------------

;程式碼段定義
code segment
     assume ds:data, ss:stack, cs:code
start:
     mov ax, data
     mov ds, ax
     mov ax, stack
     mov ss, ax
     mov sp, 200
    
     address mus_freg, mus_time
     call music

exit:     
     mov ah, 4cH
     int 21h

;------------發聲-------------
gensound proc near
     push ax
     push bx
     push cx
     push dx
     push di

     mov al, 0b6H
     out 43h, al
     mov dx, 12h
     mov ax, 348ch
     div di
     out 42h, al

     mov al, ah
     out 42h, al

     in al, 61h
     mov ah, al
     or al, 3
     out 61h, al
wait1:
     mov cx, 3314
     call waitf
delay1:
     dec bx
     jnz wait1

     mov al, ah
     out 61h, al

     pop di
     pop dx
     pop cx
     pop bx
     pop ax
     ret 
gensound endp

;--------------------------
waitf proc near
      push ax
waitf1:
      in al,61h
      and al,10h
      cmp al,ah
      je waitf1
      mov ah,al
      loop waitf1
      pop ax
      ret
waitf endp
;--------------發聲呼叫函式----------------
music proc near
      xor ax, ax
freg:
      mov di, [si]
      cmp di, 0FFFFH
      je end_mus
      mov bx, ds:[bp]
      call gensound
      add si, 2
      add bp, 2
      jmp freg
end_mus:
      ret
music endp

code ends
     end start

注意:在實現過程中可能會遇到這樣的情況:你用的是DOSBOX虛擬機器進行程式設計,然後進行測試之後,一切通過。但是移植到32位win7的虛擬8086就沒有任何聲音了。這是我親身體會的經歷。自己也不是很清楚這到底是問什麼,會因為音效卡的設計不一樣?還是說不同的音效卡有不同的控制方式?就是建議大家如果合作用匯編編寫程式時,要注意平臺的統一,不然會出現許多問題。(上面的程式實在DOSBOX中測試成功的~~)

ps:2013/11/27 :經過努力的除錯和發現,結果找到了為什麼在dosbox下可以發聲在cmd中不能的原因。因為dosbox虛擬機器的cpu頻率比較小,所以可以一次順利的讀取到每個頻率點,但是在windows xp下執行時,cpud的頻率要比dosbox下的頻率快的多,所以當讀到第一個頻率還沒有來得及發聲,有讀到了下一個頻率,以至於頻率表已經不是一個完整的頻率了,所以不能發聲。所以如果要在windows xp下執行發聲,必須給每一次讀取頻率的下一條語句新增一個延遲函式。