Noob's investigation of VGM and DMF and SMD audio drivers

For everything that's not in any way related to PureBasic. General chat etc...
SeregaZ
Enthusiast
Enthusiast
Posts: 617
Joined: Fri Feb 20, 2009 9:24 am
Location: Almaty (Kazakhstan. not Borat, but Triple G)
Contact:

Noob's investigation of VGM and DMF and SMD audio drivers

Post by SeregaZ »

i try to make some vgm player (this type of file - is a music rip from some console games). and i have a looot a questions... and problem. i know i know - you can say what for this player? it already have some nice winamp plugin, and even open sourse of player... but my plan just for one chip from whole cpu supporting of VGM - sega mega drive - and it will be need in future for special converter for old games. i mean get music not from game, but change music inside game of this console. i know i know again - it is old centry and no need it... but our romhackers will be happy to get this software.

a lot work already done... even some music already is plays, but dll, what i use for create sound, not super... as creator of it says - you get black box. so this dll emulate that music cpu of sega mega drive - YM2612. he says many words - i not understand even half... so main problem is with register $2A - by theory this register send to cpu 1 byte of sample for playing. in this case no need to know frequense, or size - just send 1 byte by 1... but this dll probably not full support this. when i try to use it - it sounds some scratch... some kind like this:

Code: Select all

for i = 0 to samplesize
  OPN_Write(0, $2A, ReadByte(*sampleadress + i))
  Pause(22microsec)
next
for this case that dll have another command PlayDACSample(0, samplesize, *sampleadress, freq). but VGM file can have Disable DAC command, then Enable at short time - so PlayDACSample is stops of playng at this moment, and not start again after enable. by practice is sounds as eat half of sample. i not want use this PlayDACSample, i want to use $2A, as cpu do in hardware. how to organize this stream? creators says it need to use sourse of dll, not dll it self, and it have some cached problem. but it dll have a million tonn of code... i never do this my own. maybe some how can be used this dll? just some how create this special stream organization?

i am special make topic at off topic branch - if some one of god father can help - answer it. i'll make code examples, and add dll sources and dll it self. if no one answer - it is ok. will think my own...
Last edited by SeregaZ on Mon May 20, 2019 10:08 am, edited 2 times in total.
infratec
Always Here
Always Here
Posts: 6817
Joined: Sun Sep 07, 2008 12:45 pm
Location: Germany

Re: Noob's investigation of VGM

Post by infratec »

SeregaZ
Enthusiast
Enthusiast
Posts: 617
Joined: Fri Feb 20, 2009 9:24 am
Location: Almaty (Kazakhstan. not Borat, but Triple G)
Contact:

Re: Noob's investigation of VGM

Post by SeregaZ »

were to get JavaScript To PB converter? :))))) thanks. will see how it work...
infratec
Always Here
Always Here
Posts: 6817
Joined: Sun Sep 07, 2008 12:45 pm
Location: Germany

Re: Noob's investigation of VGM

Post by infratec »

Hi,

if the emulation of the dll is correct, than you need to check the busy flag
That's D7 of the status. A read on one of the 4 possible addresses.
If this bit is 0, than you can write the next byte to the DAC register.

Btw. do you know that it is a 9bit DAC?

2A represents D8 to D1
D0 is located in bit 3 of register 2C.
But you don't need to use it, since it also depends on your emulator if it can handle this.

You can get all internas of the chip in this thread:
http://gendev.spritesmind.net/forum/vie ... f=24&t=386

Happy coding :wink:

Bernd
SeregaZ
Enthusiast
Enthusiast
Posts: 617
Joined: Fri Feb 20, 2009 9:24 am
Location: Almaty (Kazakhstan. not Borat, but Triple G)
Contact:

Re: Noob's investigation of VGM

Post by SeregaZ »

easy to say... i am as always not all understand.
9bit DAC? but samples is 8 bit. it is standart mono wav file, just withou head - without 44 first bytes.

about dll - probably it no have return for procedures...
https://www.dropbox.com/s/hllc8hev1yqie ... r.rar?dl=0
SeregaZ
Enthusiast
Enthusiast
Posts: 617
Joined: Fri Feb 20, 2009 9:24 am
Location: Almaty (Kazakhstan. not Borat, but Triple G)
Contact:

Re: Noob's investigation of VGM

Post by SeregaZ »

that is illustration of main problem:

Code: Select all

Enumeration
  #Window
  
  #Play
  #Play6000
EndEnumeration

DataSection

  StartVGMFile: 
    IncludeBinary "E:\SHORT.vgm"                  
  EndOfVGMFile: 
  
EndDataSection

OPNhdll = OpenLibrary(0, "OPN_DLL.dll")

If OPNhdll
  Prototype OpenDriver(int.i)
  Prototype CloseDriver()
  Prototype OPNWrite(int.i,int2.i,int3.i)  
  Prototype OPNPlayDACSample(chip.i, size.i, *Data, freq.i) 

  Global OpenOPNDriver.OpenDriver = GetFunction(0, "OpenOPNDriver")
  Global CloseOPNDriver.CloseDriver = GetFunction(0, "CloseOPNDriver")
  Global OPN_Write.OPNWrite = GetFunction(0, "OPN_Write") 
  Global PlayDACSample.OPNPlayDACSample = GetFunction(0, "PlayDACSample")
Else
  ;error
  End
EndIf

Structure VGMFSt
  type.i
  reg.a
  val.a
  pause.u
  samplenum.a
  sampleadress.i
  samplesize.i
  summofpauses.i
EndStructure
Global Dim VGMARR.VGMFSt(0)

Global autofreq = 1

; shutdown sound procedure when track is done, or stop by button
Procedure EndOfPlay(sh.i)

  OPN_Write(0, $2B, $00) ; Disable Dac
  
  ; probably RR param need to high level for shutdown
  OPN_Write(0, $80 + sh, $0F)
  OPN_Write(0, $81 + sh, $0F)
  OPN_Write(0, $82 + sh, $0F)
  OPN_Write(0, $84 + sh, $0F)
  OPN_Write(0, $85 + sh, $0F)
  OPN_Write(0, $86 + sh, $0F)
  OPN_Write(0, $88 + sh, $0F)
  OPN_Write(0, $89 + sh, $0F)
  OPN_Write(0, $8A + sh, $0F)
  OPN_Write(0, $8C + sh, $0F)
  OPN_Write(0, $8D + sh, $0F)
  OPN_Write(0, $8E + sh, $0F)
 
  If sh = 0
    OPN_Write(0, $28, $00) ; turn off
    OPN_Write(0, $28, $01)
    OPN_Write(0, $28, $02)
  Else
    OPN_Write(0, $28, $04)
    OPN_Write(0, $28, $05)
    OPN_Write(0, $28, $06) 
  EndIf
  
EndProcedure

; procedure work in another thread
Procedure ReadAndPlay(*Value) 
  
  ;get vgm adress without head
  ot = ?StartVGMFile + 64
  do = ?EndOfVGMFile
  
  ; reset main array
  Dim VGMARR(0) 
  
  Number.a = 0
  WavAddres = 0
  Arrayind = 0
  samplecoordinates = -1
  
  ; cycle of reading
  For i = ot To do
    
    ; Reads an ascii character (1 byte) from the specified memory address. 
    Number = PeekA(i)
    
    Select Number
        
      Case $67 ; wav data block adress
        
        ; Reads an integer (4 bytes in 32-bit executable, 8 bytes in 64-bit executable) number from the specified memory address. 
        WavDataSize = PeekI(i + 3)
        
        ; get this adress in mem
        WavAddres = i + 7
        
        ; jump reads to end of wav data block
        i + 6 + WavDataSize
        
      Case $52
        VGMARR(Arrayind)\type = 1
        VGMARR(Arrayind)\reg = PeekA(i+1) ; get register
        VGMARR(Arrayind)\val = PeekA(i+2) ; get value of register
        
        ; temporaly make ignore $2A. maybe somewhen i fix it to right
        If VGMARR(Arrayind)\reg <> $2A 
          Arrayind + 1
          ReDim VGMARR(Arrayind) ; +1 to array size
        EndIf
        i+2 ; make jump to next pointer
        
      Case $53
        VGMARR(Arrayind)\type = 2
        VGMARR(Arrayind)\reg = PeekA(i+1)
        VGMARR(Arrayind)\val = PeekA(i+2)
        Arrayind + 1
        ReDim VGMARR(Arrayind)
        i+2 
        
      Case $61 ; $61 - can range from 0 to 65535 (approx 1.49 seconds)
        VGMARR(Arrayind)\type = 3
        VGMARR(Arrayind)\pause = PeekU(i + 1)
        
        ; it is not very correct... but if i will have working $2A - it will no need this.
        If flagpausehunt
          VGMARR(numforsizecount)\summofpauses + VGMARR(Arrayind)\pause
          flagpausehunt = 0
        EndIf
        
        Arrayind + 1
        ReDim VGMARR(Arrayind)
        i+2
        
      Case $70 To $7F ; wait n+1 samples, n can range from 0 to 15.
        VGMARR(Arrayind)\type = 3
        VGMARR(Arrayind)\pause = ((Number - $70) + 1)
        If flagpausehunt
          VGMARR(numforsizecount)\summofpauses + VGMARR(Arrayind)\pause
          flagpausehunt = 0
        EndIf
        Arrayind + 1
        ReDim VGMARR(Arrayind)
        
      Case $E0 ; 0xE0	dddddddd	seek to offset dddddddd (Intel byte order) in PCM data bank
        ; prepear adress for next 0x8n
        samplecoordinates = PeekI(i + 1)
        samplecoordinates = WavAddres + samplecoordinates        
        
        ; jump to next command
        i + 4
        
      Case $80 To $8F
        If samplecoordinates > -1
          VGMARR(Arrayind)\type = 4
          VGMARR(Arrayind)\sampleadress = samplecoordinates
          VGMARR(Arrayind)\summofpauses = 0
          numforsizecount = Arrayind ; remember array position for count size and freq
          
          Arrayind + 1
          ReDim VGMARR(Arrayind)
          samplecoordinates = -1 ; no need more, just for once work per sample
        EndIf
        
        ; start collect size per each 0x8n
        VGMARR(numforsizecount)\samplesize = VGMARR(numforsizecount)\samplesize + 1
        
        flagpausehunt = 1 ; flag for start collect pauses too
        
        ; get n - as pause, and remember it, if exists
        If Number > $80
          VGMARR(Arrayind)\type = 3
          VGMARR(Arrayind)\pause = (Number - $80)
          VGMARR(numforsizecount)\summofpauses + VGMARR(Arrayind)\pause
          Arrayind + 1
          ReDim VGMARR(Arrayind)
        EndIf
        
      Case $4F ; 0x4F	dd	Game Gear PSG stereo, write dd to port 0x06 ; dune with samples
        ; ignore until ValleyBell add support SN76489/SN76496 to OPN.dll :)))))))
        i + 1
        
      Case $50 ; 0x50	dd	PSG (SN76489/SN76496) write value dd ; dune with samples
        ; again ignore and jump far
        i + 1
        
      Case $62 ; wait 735 samples (60th of a second), a shortcut for 0x61 0xdf 0x02 ; Lego Tune
        VGMARR(Arrayind)\type = 3
        VGMARR(Arrayind)\pause = 735
        If flagpausehunt
          VGMARR(numforsizecount)\summofpauses + VGMARR(Arrayind)\pause
          flagpausehunt = 0
        EndIf
        Arrayind + 1
        ReDim VGMARR(Arrayind)
        
      Case $63 ; wait 882 samples (50th of a second), a shortcut For 0x61 0x72 0x03
        VGMARR(Arrayind)\type = 3
        VGMARR(Arrayind)\pause = 882
        If flagpausehunt
          VGMARR(numforsizecount)\summofpauses + VGMARR(Arrayind)\pause
          flagpausehunt = 0
        EndIf
        Arrayind + 1
        ReDim VGMARR(Arrayind)
        
      Case $66 ; end of sound
        Break  
        
    EndSelect
    
  Next
  
  
  
  ; it is for organization of pauses less, than millisec
  PlayedTicks.i = 0
  PlayedUS.i = 0
  CurrentUS.i = 0
  StartMS.i = ElapsedMilliseconds()
  
  ; cycle of playing
  For i = 0 To ArraySize(VGMARR())-1
    Select VGMARR(i)\type
        
      Case 1
        OPN_Write(0, VGMARR(i)\reg, VGMARR(i)\val)
        
      Case 2
        ; probably cpu have some command with $53, but no need +256... now i set for all +256
        tmp = VGMARR(i)\reg+256
        OPN_Write(0, tmp, VGMARR(i)\val)
        
      Case 3        
        ; that cursed pause...        
        PlayedTicks + VGMARR(i)\pause
        PlayedUS = PlayedTicks * 22.675736961 ;(this 22 - As 1s/44100hz. and 22 it means microsec)
        While (CurrentUS < PlayedUS)
          Delay(1)
          CurrentUS.i = (ElapsedMilliseconds() - StartMS) * 1000
        Wend
        
      Case 4
        If autofreq = 1
          ; count frequency
          freq = VGMARR(i)\samplesize / (VGMARR(i)\summofpauses * 0.000022675736961)
          PlayDACSample(0, VGMARR(i)\samplesize, VGMARR(i)\sampleadress, freq)
        Else
          PlayDACSample(0, VGMARR(i)\samplesize, VGMARR(i)\sampleadress, 6000)
        EndIf
        
    EndSelect
  Next
  
  ; shutdown of sound
  EndOfPlay(0)
  EndOfPlay(256)
  
  ; enable plays button
  DisableGadget(#Play, 0)
  DisableGadget(#Play6000, 0)
  
EndProcedure

OpenOPNDriver(1)

If OpenWindow(#Window, 100, 100, 300, 100, "play", #PB_Window_SystemMenu | #PB_Window_MinimizeGadget | #PB_Window_ScreenCentered)
  
  ButtonGadget(#Play, 10, 10, 200, 30, "play auto freq")
  
  ButtonGadget(#Play6000, 10, 50, 200, 30, "play 6k freq")
  
  OPN_Write(0, $2B, $80)
  
  Repeat
    
    Event = WaitWindowEvent()
    
    Select Event

       Case #PB_Event_Gadget
         
         Select EventGadget()
           Case #Play
             DisableGadget(#Play, 1) ; disable for avoid double press button
             DisableGadget(#Play6000, 1)
             autofreq = 1
             CreateThread(@ReadAndPlay(), 1)
             
           Case #Play6000
             DisableGadget(#Play, 1)
             DisableGadget(#Play6000, 1)
             autofreq = 0
             CreateThread(@ReadAndPlay(), 1)

             
         EndSelect
         
       Case #PB_Event_CloseWindow
         stopflag = 1
         Quit = 1
     EndSelect
   Until Quit = 1
   
   CloseOPNDriver()
   CloseLibrary(0)
   
EndIf
https://www.dropbox.com/s/wbfg6bcsf94gzcc/demo.zip?dl=1

this third snare is eat when plays 12k frequency. with 6k plays fine. probably this PlayDac function have some caches and when it work 12k it is very fast and cache is end, when comes DAC Disable command. and plays is stop. with 6k PlayDac work slower and cache not depleted when this DAC Disable comes and then DAC Enable again at short time...
SeregaZ
Enthusiast
Enthusiast
Posts: 617
Joined: Fri Feb 20, 2009 9:24 am
Location: Almaty (Kazakhstan. not Borat, but Triple G)
Contact:

Re: Noob's investigation of VGM

Post by SeregaZ »

some man give me a few url with code of emulators. i think code from javascript and... aaaa... C? the same. but probably C will be a little better for my understand.
infratec, can you see by one eye? it will work?
https://github.com/ekeeke/Genesis-Plus- ... d/ym2612.c
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Noob's investigation of VGM

Post by wilbert »

Attempt to emulate the SN76489 chip :)

SN76489

Code: Select all

; SN76489 module v1.08 by Wilbert
; modeled after ActionScript code by Shiru

DeclareModule SN76489
  
  Declare SN76489()
  Declare Clock(freq.l, chip = 0)
  Declare Reset(chip = 0)
  Declare Render(*buf, len.l)
  Declare GGStereoWrite(val.a, chip = 0)
  Declare Write(val.a, chip = 0)
  
EndDeclareModule

Module SN76489
  
  EnableExplicit
  DisableDebugger  
  EnableASM
  
  ;- Structures
  
  Structure Chip      ; 100 bytes
    ticksCount.l      ; offset 0
    ticksPerSample.l  ; offset 4
    vol.a[4]          ; offset 8
    div.u[4]          ; offset 12
    cnt.w[4]          ; offset 20
    out.w[4]          ; offset 28
    noiseLFSR.u       ; offset 36
    noiseTap.u        ; offset 38
    channel_mix.l[4]  ; offset 40
    lpf.l[4]          ; offset 56
    latchedChan.u     ; offset 72
    latchedVol.u      ; offset 74
    reserved.l[6]     ; offset 76
  EndStructure
  
  Structure SN76489_State
    vt.w[32]          ; offset 0    volume table
    flags.l           ; offset 64   bit 0 = dual chip
    chip.Chip[2]      ; offset 68
  EndStructure
    
  ;-Global variables
  
  Global SN76489_State.SN76489_State
  
  ;- Render core
  
  CompilerIf #PB_Compiler_Processor = #PB_Processor_x86
    Macro rax : eax : EndMacro
    Macro rbx : ebx : EndMacro
    Macro rcx : ecx : EndMacro
    Macro rdx : edx : EndMacro
    Macro rsi : esi : EndMacro
    Macro rdi : edi : EndMacro
  CompilerEndIf  
  
  Macro M_Channel(chip, channel)
    sub word [rsi+20 + chip*100 + channel*2], 1     ; cnt - 1
    !jnc .c1#chip#channel                           ; cnt >= 0 => c1#channel
    movzx ecx, word [rsi+12 + chip*100 + channel*2] ; get div
    CompilerIf channel = 3
      !and ecx, 3
      !mov eax, 0x10
      !shl al, cl
      !jns .n#chip
      movzx eax, word [rsi+12 + chip*100 + 4]       ; get divC
      !.n#chip:
      mov ecx, eax                                  ; ecx = cntD
      movzx eax, word [rsi+38 + chip*100]           ; noiseTap
      And ax, [rsi+36 + chip*100]                   ; apply to noiseLFSR 
      !xor al, ah                                   ; get parity
      !setnp al
      !shl eax, 16
      add ax, [rsi+36 + chip*100]
      !shr eax, 1
      mov [rsi+36 + chip*100], ax                   ; update noiseLFSR
      !xor eax, 1                                   ; invert bit because of volume table structure
      !shl eax, 12
      mov al, [rsi+8 + chip*100 + 3]                ; get volD
      !and al, 15
      !or al, ah
      !and eax, 31
    CompilerElse
      !cmp ecx, 1                                   ; div <= 1 => c0#channel
      !jna .c0#chip#channel
      movzx eax, byte [rsi+8 + chip*100 + channel]  ; get vol
      !xor eax, 16                                  ; vol ! 16
    CompilerEndIf
    mov [rsi+8 + chip*100 + channel], al            ; set vol
    movsx eax, word [rsi-68 + rax*2]                ; lookup vol in table
    mov [rsi+28 + chip*100 + channel*2], ax         ; out = lookup value
    !.c0#chip#channel:
    mov [rsi+20 + chip*100 + channel*2], cx         ; set cnt
    !.c1#chip#channel:
    ; simple low pass filter
    movsx eax, word [rsi+28 + chip*100 + channel*2]
    mov ecx, [rsi+56 + chip*100 + channel*4]
    sub eax, ecx
    sar eax, 1                                      ; lpf alpha 0.5 (div by 2)
    add eax, ecx
    mov [rsi+56 + chip*100 + channel*4], eax
  EndMacro
  
  Macro M_Tick(chip)
    sub dword [rsi + chip*100], 0x01000000          ; update ticksCount
    !ja .rloop#chip
    mov eax, [rsi+4 + chip*100]                     ; ticksCount+=ticksPerSample
    add [rsi + chip*100], eax
  EndMacro
    
  Macro M_ChannelOut(chip, channel)
    movzx ecx, word [rsi+56 + chip*100 + channel*4]
    imul ecx, [rsi+40 + chip*100 + channel*4]       ; apply channel mix
    !add eax, ecx
    !and eax, 0xfffefffe
  EndMacro
  
  Procedure Render(*buf, len.l)
    !mov ecx, [p.v_len]
    !test ecx, ecx
    !jz .exit
    mov rax, *buf
    mov rdx, SN76489_State
    push rbx
    push rsi
    push rdi
    lea rsi, [rdx+68]                             ; point to first chip 
    mov rdi, rax
    mov ebx, ecx
    mov edx, [rsi-4]    
    !.rloop0:
    M_Channel(0,0)
    M_Channel(0,1)
    M_Channel(0,2)
    M_Channel(0,3)
    M_Tick(0)
    !bt edx, 0
    !jnc .rskip0 
    !.rloop1:
    M_Channel(1,0)
    M_Channel(1,1)
    M_Channel(1,2)
    M_Channel(1,3)
    M_Tick(1)
    !.rskip0:
    !xor eax, eax
    M_ChannelOut(0,0)
    M_ChannelOut(0,1)
    M_ChannelOut(0,2)
    M_ChannelOut(0,3)
    !bt edx, 0
    !jnc .rskip1
    M_ChannelOut(1,0)
    M_ChannelOut(1,1)
    M_ChannelOut(1,2)
    M_ChannelOut(1,3)
    !.rskip1:
    mov [rdi], eax                                ; write samples
    add rdi, 4
    sub ebx ,1
    !jnz .rloop0
    pop rdi
    pop rsi
    pop rbx
    !.exit:
  EndProcedure
  
  ;- Other procedures
  
  Procedure Clock(freq.l, chip = 0)
    mov rdx, SN76489_State
    add rdx, 68                                   ; point to first chip
    !mov eax, [p.v_freq]
    !bt eax, 30
    !setc cl
    mov [rdx-4], cl
    !and eax, 0x003fffff
    !cdq
    !mov ecx, 0x01000000
    !mul ecx
    !mov ecx, 16 * 44100
    !div ecx
    !mov ecx, [p.v_chip]
    !and ecx, 1
    !imul ecx, 100
    mov rdx, SN76489_State
    add rdx, 68                                   ; point to first chip
    mov [rdx+4 + rcx], eax
  EndProcedure
  
  Procedure Reset(chip = 0)
    Protected i.i
    With SN76489_State\chip[chip]
      \ticksCount = \ticksPerSample
      For i = 0 To 3
        \vol[i] = 15 : \div[i] = 1 : \cnt[i] = 0 : \out[i] = 0
        \channel_mix[i] = $00010001 : \lpf[i] = 0
      Next
      \noiseLFSR = $8000 : \div[3] = 0 : \noiseTap = 9
      \latchedChan = 0 : \latchedVol = 0
    EndWith
  EndProcedure
  
  Procedure SN76489()
    Protected i.i
    With SN76489_State
      \vt[0] = 4095 : \vt[1] = 4095 : \vt[2] = 4095
      \vt[3] = 3489 : \vt[4] = 2860 : \vt[5] = 2282
      \vt[6] = 1855 : \vt[7] = 1483 : \vt[8] = 1180
      \vt[9] =  909 : \vt[10] = 730 : \vt[11] = 565
      \vt[12] = 441 : \vt[13] = 344 : \vt[14] = 275
      \vt[15] = 0
      For i = 0 To 15 : \vt[i + 16] = -0.3 * \vt[i] : Next
    EndWith
    Clock(3579545) : Clock(3579545, 1)
    Reset() : Reset(1)
  EndProcedure
  
  Procedure Write(val.a, chip = 0); bit 8 = chip 0 / 1
    Protected.l chan, cdiv
    chip & 1
    With SN76489_State\chip[chip]
      If val & $80
        chan = val >> 5 & 3
        \latchedChan = chan
        \latchedVol = val >> 4 & 1
        cdiv = (\div[chan] & $3f0) | (val & $f)
      Else
        chan = \latchedChan
        cdiv = (\div[chan] & $f) | (val << 4 & $3f0)
      EndIf
      If \latchedVol
        \vol[chan] = (\vol[chan] & $10) | (val & $f)
        If cdiv = 1
          \out[chan] = SN76489_State\vt[val & $f]
        EndIf  
      Else
        \div[chan] = cdiv
        If chan = 3
          \noiseTap = cdiv << 1 & 8 + 1
          \noiseLFSR = $8000      
        EndIf
      EndIf
    EndWith
  EndProcedure
  
  Procedure GGStereoWrite(val.a, chip = 0); bit 8 = chip 0 / 1
    Protected.l i
    chip & 1
    For i = 0 To 3
      SN76489_State\chip[chip]\channel_mix[val >> 6 & 4 + i] = (val >> (i+4) & 1) | (val << (16-i) & $10000)
    Next
  EndProcedure
    
EndModule
Windows part

Code: Select all

UseModule SN76489
SN76489()

#PSG_BUFFERS = 4
Global Dim wavebuf.w(#PSG_BUFFERS - 1, 2047)
Global Dim wavehdr.WAVEHDR(#PSG_BUFFERS - 1)
Global.i PSG_Unit, WMME_Playing

DataSection
  PCM16BitSigned:
  Data.w 1,2,$AC44,0,$B110,2,4,16,0
EndDataSection  

Procedure WMME_Callback(hwo, uMsg, dwInstance, *wavehdr.WAVEHDR, dwParam2)
  If WMME_Playing And uMSG = #WOM_DONE
    Render(*wavehdr\lpData, 1024)
    waveOutWrite_(hwo, *wavehdr, SizeOf(WAVEHDR))
  EndIf
EndProcedure

Procedure PSG_Terminate()
  Protected i.i
  If PSG_Unit
    WMME_Playing = #False
    waveOutReset_(PSG_Unit)
    While i < #PSG_BUFFERS     
      waveOutUnprepareHeader_(PSG_Unit, @wavehdr(i), SizeOf(WAVEHDR))
      i + 1
    Wend        
    waveOutClose_(PSG_Unit)
    PSG_Unit = 0
  EndIf
EndProcedure  

Procedure PSG_Init()
  If waveOutOpen_(@PSG_Unit, #WAVE_MAPPER, ?PCM16BitSigned, @WMME_Callback(), 0, #CALLBACK_FUNCTION)
    MessageRequester("Error", "Unable to initialize PSG")
  Else
    While i < #PSG_BUFFERS
      wavehdr(i)\lpData = @wavebuf(i, 0)
      wavehdr(i)\dwBufferLength = 4096
      waveOutPrepareHeader_(PSG_Unit, @wavehdr(i), SizeOf(WAVEHDR))
      waveOutWrite_(PSG_Unit, @wavehdr(i), SizeOf(WAVEHDR))
      i + 1
    Wend
    WMME_Playing = #True   
  EndIf
EndProcedure

PSG_Init()
After this insert the write codes to write to the emulated chip.
Last edited by wilbert on Sat Sep 24, 2016 4:17 pm, edited 20 times in total.
Windows (x64)
Raspberry Pi OS (Arm64)
SeregaZ
Enthusiast
Enthusiast
Posts: 617
Joined: Fri Feb 20, 2009 9:24 am
Location: Almaty (Kazakhstan. not Borat, but Triple G)
Contact:

Re: Noob's investigation of VGM

Post by SeregaZ »

http://mnp.tut.su/covbatmanrotj.vgm

Code: Select all

;{
   ProgName.s=GetFilePart(ProgramFilename()) 
   a = CreateSemaphore_(#Null,0,1,@ProgName) 
   If a<>0 And GetLastError_()=#ERROR_ALREADY_EXISTS 
     CloseHandle_(a) 
     End 
   EndIf
;} 
   
Enumeration
  
  #Window
  
  #PathString
  #OpenButton
  #File
  
  #Text
  
EndEnumeration  
  
DeclareModule SN76489
  
  Declare SN76489()
  Declare Clock(freq.l)
  Declare Reset()
  Declare Render(*buf, len.l)
  Declare Write(val.a)
  
EndDeclareModule

Module SN76489
  
  #MONO_RENDER = #True
  
  EnableExplicit
  DisableDebugger  
  EnableASM
  
  ;- Structures
  
  Structure SN76489_State
    ticksCount.l      ; offset 0
    ticksPerSample.l  ; offset 4
    vol.a[4]          ; offset 8
    div.u[4]          ; offset 12
    cnt.w[4]          ; offset 20
    out.w[4]          ; offset 28
    noiseLFSR.u       ; offset 36
    noiseTap.u        ; offset 38
    latchedChan.u     ; offset 40
    latchedVol.u      ; offset 42
    vt.u[32]          ; offset 44
  EndStructure
  
  ;-Global variables
  
  Global SN76489_State.SN76489_State
  
  ;- Render core
  
  CompilerIf #PB_Compiler_Processor = #PB_Processor_x86
    Macro rax : eax : EndMacro
    Macro rbx : ebx : EndMacro
    Macro rdx : edx : EndMacro
    Macro rdi : edi : EndMacro
  CompilerEndIf  
  
  Macro M_Channel(channel)
    sub word [rdx + 20 + channel * 2], 1      ; cnt - 1
    !jnc .c1#channel                          ; cnt < 0 => c1#channel
    movzx ecx, word [rdx + 12 + channel * 2]  ; get div
    CompilerIf channel = 3
      !and ecx, 3
      !mov eax, 0x10
      !shl al, cl
      !jns .n0
      movzx eax, word [rdx + 16]              ; get divC
      !shl eax, 1                             ; divC << 1
      !.n0:
      mov ecx, eax                            ; ecx = cntD
      movzx eax, word [rdx + 36]              ; get noiseLFSR 
      cmp word [rdx + 38], 9                  ; noiseTap 9 => .n1
      !jne .n1
      !and eax, 9                             ; tap & 9
      !xor al, ah                             ; get parity
      !setnp al
      !.n1:
      !and eax, 1
      !shl eax, 16
      add ax, [rdx + 36]
      !shr eax, 1
      mov [rdx + 36], ax                      ; update noiseLFSR
      !xor eax, 1
      !shl eax, 12
      mov al, [rdx + 11]                      ; get volD
      !and al, 15
      !or al, ah
      !and eax, 31
    CompilerElse
      !cmp ecx, 1                             ; div <= 1 => c0#channel
      !jna .c0#channel
      movzx eax, byte [rdx + 8 + channel]     ; get vol
      !xor eax, 16                            ; vol ! 16
    CompilerEndIf
    mov [rdx + 8 + channel], al               ; set vol
    movzx eax, word [rdx + 44 + rax * 2]      ; lookup vol in table
    mov [rdx + 28 + channel * 2], ax          ; out = lookup value
    !.c0#channel:
    mov [rdx + 20 + channel * 2], cx          ; set cnt
    !.c1#channel:
  EndMacro
  
  Macro M_ChannelOut(channel)
    movsx ecx, word [rdx + 28 + channel * 2]
    add eax, ecx
    imul ecx, 0xfe00
    sar ecx, 16
    mov [rdx + 28 + channel * 2], cx
  EndMacro
  
  Procedure Render(*buf, len.l)
    !mov ecx, [p.v_len]
    !test ecx, ecx
    !jz .exit
    mov rax, *buf
    mov rdx, SN76489_State
    push rbx
    push rdi
    mov rdi, rax
    mov ebx, ecx   
    !.rloop:
    M_Channel(0)
    M_Channel(1)
    M_Channel(2)
    M_Channel(3)
    sub dword [rdx], 0x01000000               ; update ticksCount
    !ja .rloop
    mov eax, [rdx + 4]                        ; ticksCount+=ticksPerSample
    add [rdx], eax
    sub eax, eax
    M_ChannelOut(0)
    M_ChannelOut(1)
    M_ChannelOut(2)
    M_ChannelOut(3)
    !sar eax, 1
    cmp eax, 32768
    !jl .clip0
    mov eax, 32767                            ; clip output
    !jmp .clip1
    !.clip0:
    cmp eax,-32768
    !jg .clip1
    mov eax,-32767
    !.clip1:    
    mov [rdi], ax                             ; write samples
    CompilerIf #MONO_RENDER
      add rdi, 2
    CompilerElse
      mov [rdi + 2], ax
      add rdi, 4
    CompilerEndIf
    sub ebx ,1
    !jnz .rloop
    pop rdi
    pop rbx
    !.exit:
  EndProcedure
  
  ;- Other procedures
  
  Procedure Clock(freq.l)
    !mov eax, [p.v_freq]
    !cdq
    !mov ecx, 0x01000000
    !mul ecx
    !mov ecx, 16 * 44100
    !div ecx
    mov rdx, SN76489_State
    mov [rdx + 4], eax
  EndProcedure
  
  Procedure Reset()
    FillMemory(@SN76489_State\vol[0], 4, 15)  ; reset volume
    FillMemory(@SN76489_State\div[0], 32)     ; clear rest of state
    SN76489_State\noiseLFSR = $f037
    SN76489_State\ticksCount = SN76489_State\ticksPerSample
  EndProcedure
  
  Procedure SN76489()
    Protected i.i, delitmer.i
    
    ;- DELETE IT
    ;MY ADDITIONAL - SET DELITMER TO 1 FOR ORIGIMAL VOLUME
    ;i think / 4 will avoid clip cases, when all 4 channel have 
    ;max value after mixing channels. maybe i am wrong, as always :)
    delitmer = 4
    
    With SN76489_State
      \vt[0] = 32767 / delitmer : \vt[1] = 26028 / delitmer : \vt[2] = 20675 / delitmer
      \vt[3] = 16422 / delitmer : \vt[4] = 13045 / delitmer : \vt[5] = 10362 / delitmer
      \vt[6] =  8231 / delitmer : \vt[7] =  6568 / delitmer : \vt[8] =  5193 / delitmer
      \vt[9] =  4125 / delitmer : \vt[10] = 3277 / delitmer : \vt[11] = 2603 / delitmer
      \vt[12] = 2067 / delitmer : \vt[13] = 1642 / delitmer : \vt[14] = 1304 / delitmer
      \vt[15] = 0
      For i = 0 To 15
        \vt[i + 16] = -\vt[i]
      Next
    EndWith
    Clock(3579545)
    Reset()
  EndProcedure
  
  Procedure Write(val.a)
    Protected.u chan, cdiv, div, vol
    If val & $80
      chan = val >> 5 & 3
      cdiv = SN76489_State\div[chan] & $fff0 | (val & $f)
      SN76489_State\latchedChan = chan
      SN76489_State\latchedVol = val >> 4 & 1
    Else
      chan = SN76489_State\latchedChan
      cdiv = SN76489_State\div[chan] & $f | (val << 4 & $3f0)
    EndIf
    If SN76489_State\latchedVol
      SN76489_State\vol[chan] = SN76489_State\vol[chan] & $10 | (val & $f)
    Else
      SN76489_State\div[chan] = cdiv
      If chan = 3
        SN76489_State\noiseTap = cdiv << 1 & 8 + 1
        SN76489_State\noiseLFSR = $8000      
      EndIf
    EndIf
  EndProcedure
  
EndModule

UseModule SN76489
SN76489()

#PSG_BUFFERS = 4
Global Dim wavebuf.w(#PSG_BUFFERS - 1, 1023)
Global Dim wavehdr.WAVEHDR(#PSG_BUFFERS - 1)
Global.i PSG_Unit, WMME_Playing

DataSection
  PCM16BitSigned:
  Data.w 1,1,$AC44,0,$5888,1,2,16,0
EndDataSection  

Procedure WMME_Callback(hwo, uMsg, dwInstance, *wavehdr.WAVEHDR, dwParam2)
  If WMME_Playing And uMSG = #WOM_DONE
    Render(*wavehdr\lpData, 1024)
    waveOutWrite_(hwo, *wavehdr, SizeOf(WAVEHDR))
  EndIf
EndProcedure

Procedure PSG_Terminate()
  Protected i.i
  If PSG_Unit
    WMME_Playing = #False
    waveOutReset_(PSG_Unit)
    While i < #PSG_BUFFERS     
      waveOutUnprepareHeader_(PSG_Unit, @wavehdr(i), SizeOf(WAVEHDR))
      i + 1
    Wend        
    waveOutClose_(PSG_Unit)
    PSG_Unit = 0
  EndIf
EndProcedure  

Procedure PSG_Init()
  If waveOutOpen_(@PSG_Unit, #WAVE_MAPPER, ?PCM16BitSigned, @WMME_Callback(), 0, #CALLBACK_FUNCTION)
    MessageRequester("Error", "Unable to initialize PSG")
  Else
    While i < #PSG_BUFFERS
      wavehdr(i)\lpData = @wavebuf(i, 0)
      wavehdr(i)\dwBufferLength = 2048
      waveOutPrepareHeader_(PSG_Unit, @wavehdr(i), SizeOf(WAVEHDR))
      waveOutWrite_(PSG_Unit, @wavehdr(i), SizeOf(WAVEHDR))
      i + 1
    Wend
    WMME_Playing = #True   
  EndIf
EndProcedure

PSG_Init()

;Procedure Spam(*Value)
;  
;  Repeat
;    
;  XIncludeFile "spam.pb"
;
;  ForEver
;  
;EndProcedure

;CreateThread(@Spam(), 154)

Structure VGMFSt
  type.i
  reg.a
  val.a
  pause.u
  samplenum.a
  sampleadress.i
  samplesize.i
  summofpauses.i
EndStructure
Global Dim VGMARR.VGMFSt(0)

Global TormozFlag = 1
Global PlThr

;{
Macro SetBit(Var, Bit)
  Var | (Bit)
EndMacro
 
Macro ClearBit(Var, Bit)
  Var & (~(Bit))
EndMacro

Macro TestBit(Var, Bit)
  Bool(Var & (Bit))
EndMacro
 
Macro NumToBit(Num)
  (1<<(Num))
EndMacro

Macro GetBits(Var, StartPos, EndPos)
  ((Var>>(StartPos))&(NumToBit((EndPos)-(StartPos)+1)-1))
EndMacro
;}

Procedure Play(*Value)
  
  PlayedTicks.i = 0
  PlayedUS.i = 0
  CurrentUS.i = 0
  StartMS.i = ElapsedMilliseconds()

  For i = start To ArraySize(VGMARR())-1

    Select VGMARR(i)\type
      Case 3 ; pauses    
        PlayedTicks + VGMARR(i)\pause
        PlayedUS = PlayedTicks * 22.675736961;90.702947844;22.675736961
        While (CurrentUS < PlayedUS)
          Delay(1)
          CurrentUS.i = (ElapsedMilliseconds() - StartMS) * 1000
        Wend

      Case 5 ; PSG
        Write(VGMARR(i)\val)

    EndSelect
    
    If TormozFlag      
      Break
    EndIf 
    
  Next
  
EndProcedure

Procedure ParsePlay(*FileMem)
  
  memsize = MemorySize(*FileMem)

  ;get version of vgm
  ver$ = ""
  tmp = PeekA(*FileMem + 11)
  ver$ = Str(GetBits(tmp, 4, 7))
  ver$ + Str(GetBits(tmp, 0, 3))
  tmp = PeekA(*FileMem + 10)
  ver$ + Str(GetBits(tmp, 4, 7))
  ver$ + Str(GetBits(tmp, 0, 3))
  tmp = PeekA(*FileMem + 9)
  ver$ + Str(GetBits(tmp, 4, 7))
  ver$ + Str(GetBits(tmp, 0, 3))
  tmp = PeekA(*FileMem + 8)
  ver$ + Str(GetBits(tmp, 4, 7))
  ver$ + Str(GetBits(tmp, 0, 3))

  If Val(ver$) < 151
    ot = *FileMem + 64      ; 64 - it is vgm header. no need it yet
  Else
    ot = *FileMem + 256
  EndIf  
  do = *FileMem + memsize

  ; create array with size as filesize. array will get less size, than file, but it is ok
  Dim VGMARR(memsize)

  Number.a = 0
  PSGvalue.a
  Arrayind = 0  
  
  For i = ot To do
    
    Number = PeekA(i)
    
    Select Number
        
      Case $67  ;  0x67 0x66 tt ss ss ss ss (data)
        ; it is big wav data block        
        ; get size of wav data block
        WavDataSize = PeekI(i + 3)
        ; get adress of wav data block
        WavAddres = i + 7
        ; jump to end of wav data block
        i + 6 + WavDataSize 
        
      Case $52 ; $52 - register +0
        i+2
        
      Case $53 ; $53 - register +256
        i+2 
        
      Case $61 ; $61 - can range from 0 to 65535 (approx 1.49 seconds)
        VGMARR(Arrayind)\type = 3
        VGMARR(Arrayind)\pause = PeekU(i + 1)
        If flagpausehunt
          VGMARR(numforsizecount)\summofpauses + VGMARR(Arrayind)\pause
          flagpausehunt = 0
        EndIf
        Arrayind + 1
        i+2
        
      Case $70 To $7F ; wait n+1 samples, n can range from 0 to 15.
        VGMARR(Arrayind)\type = 3
        VGMARR(Arrayind)\pause = ((Number - $70) + 1)
        If flagpausehunt
          VGMARR(numforsizecount)\summofpauses + VGMARR(Arrayind)\pause
          flagpausehunt = 0
        EndIf
        Arrayind + 1
        
      Case $E0 ; 0xE0	dddddddd	seek to offset dddddddd (Intel byte order) in PCM data bank
        ;jump far
        i + 4
        
      Case $80 To $8F
        flagpausehunt = 1        
        If Number > $80
          VGMARR(Arrayind)\type = 3
          VGMARR(Arrayind)\pause = (Number - $80)
          VGMARR(numforsizecount)\summofpauses + VGMARR(Arrayind)\pause
          Arrayind + 1
        EndIf
        
      Case $4F ; 0x4F	dd	Game Gear PSG stereo, write dd to port 0x06 ; dune with samples
        ; ignore, just jump far
        i + 1
        
      ;MAIN COMMAND :))
      Case $50 ; 0x50	dd	PSG (SN76489/SN76496) write value dd ; dune with samples
        i + 1
        PSGvalue = PeekA(i)
        VGMARR(Arrayind)\type = 5
        VGMARR(Arrayind)\val = PSGvalue
        Arrayind + 1
        
      Case $62 ; wait 735 samples (60th of a second), a shortcut for 0x61 0xdf 0x02 ; Lego Tune
        VGMARR(Arrayind)\type = 3
        VGMARR(Arrayind)\pause = 735
        If flagpausehunt
          VGMARR(numforsizecount)\summofpauses + VGMARR(Arrayind)\pause
          flagpausehunt = 0
        EndIf
        Arrayind + 1
        
      Case $63 ; wait 882 samples (50th of a second), a shortcut For 0x61 0x72 0x03
        VGMARR(Arrayind)\type = 3
        VGMARR(Arrayind)\pause = 882
        If flagpausehunt
          VGMARR(numforsizecount)\summofpauses + VGMARR(Arrayind)\pause
          flagpausehunt = 0
        EndIf
        Arrayind + 1
        
      Case $66 ; end of sound
        Debug "end"        
        Break 
        
        
    EndSelect
    

  Next
  
  Play(0) 
  
EndProcedure

If OpenWindow(#Window, 100, 200, 195, 260, "PureBasic Window", #PB_Window_SystemMenu | #PB_Window_MinimizeGadget | #PB_Window_MaximizeGadget)
  
  StringGadget(#PathString, 10, 20, 120, 20, "", #PB_String_ReadOnly)
  ButtonGadget(#OpenButton, 130, 20, 50, 20, "open")
  
  TextGadget(#Text, 10, 40, 170, 80, "WARNING! VGZ must be unpacked by manualy into VGM (7z can do this. i cant plug zlib yet). and see what chips was used by this VGM. it plays only SN76489. YM2612 is ignored. any VGM with other chips can crash programm.")

  Repeat
    
    Event = WaitWindowEvent()
    
    Select Event

      Case #PB_Event_Gadget
        Select EventGadget()
          ;- Event  
          Case #OpenButton
            If File$
              StandardFile$ = GetPathPart(File$)
            Else
              StandardFile$ = "C:\"        ; initial path + file
            EndIf
            Pattern$ = "VGM files (*.vgm)|*.vgm;";*.vgz|"   ; set first pattern  (index = 0)
            Pattern = 0    ; use the second of the five possible patterns as standard

            ; Now we open a filerequester, you can change the pattern and will get the index after closing
            File$ = OpenFileRequester("Please choose file to load", StandardFile$, Pattern$, Pattern)
            If File$
              SetGadgetText(#PathString, File$)
              
              If ReadFile(#File, File$)
                length = Lof(#File)                            ; get the length of opened file
                If *MemoryID
                  FreeMemory(*MemoryID)
                  *MemoryID = 0
                EndIf
                *MemoryID = AllocateMemory(length)         ; allocate the needed memory
                If *MemoryID
                  bytes = ReadData(#File, *MemoryID, length)   ; read all data into the memory block
                EndIf
                CloseFile(#File)
                
                TormozFlag = 0
                
                PlThr = CreateThread(@ParsePlay(), *MemoryID)
                
                
              EndIf

              
            EndIf
            
          EndSelect
            
            

      Case #PB_Event_CloseWindow
         Quit = 1
         
    EndSelect

  Until Quit = 1
  
EndIf

End 
now it can read part of VGM universe :)

if some one can make zlib support - you are welcome :)
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Noob's investigation of VGM

Post by wilbert »

I updated my SN76489 code above to be more close to the output of a real chip.
The volume is also decreased now. It won't clip anymore.
I read somewhere that there are also files who have a sort of audio sample in it. Do you have examples for those as well ?
Windows (x64)
Raspberry Pi OS (Arm64)
SeregaZ
Enthusiast
Enthusiast
Posts: 617
Joined: Fri Feb 20, 2009 9:24 am
Location: Almaty (Kazakhstan. not Borat, but Triple G)
Contact:

Re: Noob's investigation of VGM

Post by SeregaZ »

you mean vgm files, where samples is play by SN? no, not have yet. i ask ValleyBell and wait answer, maybe he is know what game use this.

about emulator:
vgm have command:
0x4F dd Game Gear PSG stereo, write dd to port 0x06
can your emu support it? this dd is have bits flag for all 4 channel stereo. my final destination format - GEMS audio driver probably no have stereo switch, but this can be usefull for select channels for play in player, as winamp do:
Image

by this function can be turn off any channel and play only noise for example.
SeregaZ
Enthusiast
Enthusiast
Posts: 617
Joined: Fri Feb 20, 2009 9:24 am
Location: Almaty (Kazakhstan. not Borat, but Triple G)
Contact:

Re: Noob's investigation of VGM

Post by SeregaZ »

https://dl.dropboxusercontent.com/u/1287967/ab.vgm
this one give me from forum. i try to turn off all channels in winamp and it still playing :)
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3870
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Noob's investigation of VGM

Post by wilbert »

SeregaZ wrote:vgm have command:
0x4F dd Game Gear PSG stereo, write dd to port 0x06
can your emu support it?
First I want to try to get the sample playing to work.
As for the 0x4F command, is it really used ? It sounds a bit artificial to me.
The VGM specification also mentions stereo by using a dual chip design and write to the second chip with the 0x30 command.
That sounds like a better way but again, I know very little about this VGM so I don't know if it is used in reality.
Windows (x64)
Raspberry Pi OS (Arm64)
SeregaZ
Enthusiast
Enthusiast
Posts: 617
Joined: Fri Feb 20, 2009 9:24 am
Location: Almaty (Kazakhstan. not Borat, but Triple G)
Contact:

Re: Noob's investigation of VGM

Post by SeregaZ »

this command not send into register, where "write" send. it is some... external or how it name command. VGM have it. my code ignore it, becouse my target sound driver no have this settings, but VGM have it. it need for panning left\right and turn on\off some of 4 channels.

for module it will be some global values, or it need add to main structure - and "write", or "render" will need additional "if", where will check bits for channels. for example:
255 value = 11111111 = turn on all. render or write send command all what it have. if it is not 255, by this quote:
Game Gear stereo extension
When a byte is written to port 0x06 on the Game Gear, the PSG output is affected as follows:
Bit Channel Side
0 0 Right
1 1 Right
2 2 Right
3 3 Right
4 0 Left
5 1 Left
6 2 Left
7 3 Left
If a bit is set, the corresponding channel is output to the corresponding side. So, 0xff outputs all channels to all sides, 0xf0 outputs to the left side only, etc.
about samples play - it is hard for my understanding :) as i see, when play this ab.vgm file - it used same 0x50. and how it understand about playng sample, not some tone - not clear for me.

Code: Select all

;probably some "initialization"
10000000 ; 1, 00 channel, 0 - tone, 0000 data
0             ; 0 0, 000000 - data.
10100000 ; 1 channel
0
11000000 ; 2 channel
0
11100000 ; 11 - 3 channel

10010101 ; 1, 00 channel, 1 volume, 0101 - 5 value
10110101 ; 1, 1 channel, 1 volume, 0101 - 5 value
11010101 ; 1, 2 channel, 1 volume, 0101 - 5 value
11111111 ; 1, 3 channel, 1 volume, 1111 - 15 value - silence
10011000 ; probably from this part chip must get volume 1000 = 8 not as volume for tone, but as DAC sample. but how he understand it? :)))) probably when frequency of tone is 0 - he start play volume value as one "tik" of DAC.
10010101 ; but then comes new question - how he know 8 bit, or 4 bit or 1 bit it is? :))))
10010100 ; ideal will be 4 bit - always one. low 4 bits. but 8 bits - probably he will need to wait second value and then count both of them.
10010101
10010110
10010101
maybe this is help:
Sample playback makes use of a feature of the SN76489's tone generators: when the half-wavelength (tone value) is set to 1, they output a DC offset value corresponding to the volume level (i.e. the wave does not flip-flop). By rapidly manipulating the volume, a crude form of PCM is obtained.
and this one:
If the register value is zero or one then the output is a constant value of +1. This is often used for sample playback on the SN76489.
SeregaZ
Enthusiast
Enthusiast
Posts: 617
Joined: Fri Feb 20, 2009 9:24 am
Location: Almaty (Kazakhstan. not Borat, but Triple G)
Contact:

Re: Noob's investigation of VGM

Post by SeregaZ »

about dual chip. i take Rydeen (Main BGM) track
https://vgmrips.net/packs/pack/super-locomotive-arcade

it need add x30 command, same as x50. and probably it will need add for our SN more 4 channels :) make 8 of them. and probably write command will need second param for chip. and render will need 8 channels count. and probably it will need more lower base volume. becouse 8 channels with max volumes will kill speaker :))) and i see in a log some xFF unknown command.

play 8 channel inside 4. it is a little mess...

Code: Select all

;{
   ProgName.s=GetFilePart(ProgramFilename()) 
   a = CreateSemaphore_(#Null,0,1,@ProgName) 
   If a<>0 And GetLastError_()=#ERROR_ALREADY_EXISTS 
     CloseHandle_(a) 
     End 
   EndIf
;} 
   
Enumeration
  
  #Window
  
  #PathString
  #OpenButton
  #File
  
  #Text
  
EndEnumeration 

DeclareModule SN76489
  
  Declare SN76489()
  Declare Clock(freq.l)
  Declare Reset()
  Declare Render(*buf, len.l)
  Declare Write(val.a, chip.i=0)
  
EndDeclareModule

Module SN76489
  
  #MONO_RENDER = #True
  
  EnableExplicit
  DisableDebugger  
  EnableASM
  
  ;- Structures
  
  Structure SN76489_State
    ticksCount.l      ; offset 0
    ticksPerSample.l  ; offset 4
    vol.a[4]          ; offset 8
    div.u[4]          ; offset 12
    cnt.w[4]          ; offset 20
    out.w[4]          ; offset 28
    noiseLFSR.u       ; offset 36
    noiseTap.u        ; offset 38
    latchedChan.u     ; offset 40
    latchedVol.u      ; offset 42
    vt.u[32]          ; offset 44
  EndStructure
  
  ;-Global variables
  
  Global SN76489_State.SN76489_State
  
  ;- Render core
  
  CompilerIf #PB_Compiler_Processor = #PB_Processor_x86
    Macro rax : eax : EndMacro
    Macro rbx : ebx : EndMacro
    Macro rdx : edx : EndMacro
    Macro rdi : edi : EndMacro
  CompilerEndIf  
  
  Macro M_Channel(channel)
    sub word [rdx + 20 + channel * 2], 1      ; cnt - 1
    !jnc .c1#channel                          ; cnt < 0 => c1#channel
    movzx ecx, word [rdx + 12 + channel * 2]  ; get div
    CompilerIf channel = 3
      !and ecx, 3
      !mov eax, 0x10
      !shl al, cl
      !jns .n0
      movzx eax, word [rdx + 16]              ; get divC
      !shl eax, 1                             ; divC << 1
      !.n0:
      mov ecx, eax                            ; ecx = cntD
      movzx eax, word [rdx + 36]              ; get noiseLFSR 
      cmp word [rdx + 38], 9                  ; noiseTap 9 => .n1
      !jne .n1
      !and eax, 9                             ; tap & 9
      !xor al, ah                             ; get parity
      !setnp al
      !.n1:
      !and eax, 1
      !shl eax, 16
      add ax, [rdx + 36]
      !shr eax, 1
      mov [rdx + 36], ax                      ; update noiseLFSR
      !xor eax, 1
      !shl eax, 12
      mov al, [rdx + 11]                      ; get volD
      !and al, 15
      !or al, ah
      !and eax, 31
    CompilerElse
      !cmp ecx, 1                             ; div <= 1 => c0#channel
      !jna .c0#channel
      movzx eax, byte [rdx + 8 + channel]     ; get vol
      !xor eax, 16                            ; vol ! 16
    CompilerEndIf
    mov [rdx + 8 + channel], al               ; set vol
    movzx eax, word [rdx + 44 + rax * 2]      ; lookup vol in table
    mov [rdx + 28 + channel * 2], ax          ; out = lookup value
    !.c0#channel:
    mov [rdx + 20 + channel * 2], cx          ; set cnt
    !.c1#channel:
  EndMacro
  
  Macro M_ChannelOut(channel)
    movsx ecx, word [rdx + 28 + channel * 2]
    add eax, ecx
    imul ecx, 0xff80
    sar ecx, 16
    mov [rdx + 28 + channel * 2], cx
  EndMacro
  
  Procedure Render(*buf, len.l)
    !mov ecx, [p.v_len]
    !test ecx, ecx
    !jz .exit
    mov rax, *buf
    mov rdx, SN76489_State
    push rbx
    push rdi
    mov rdi, rax
    mov ebx, ecx   
    !.rloop:
    M_Channel(0)
    M_Channel(1)
    M_Channel(2)
    M_Channel(3)
    sub dword [rdx], 0x01000000               ; update ticksCount
    !ja .rloop
    mov eax, [rdx + 4]                        ; ticksCount+=ticksPerSample
    add [rdx], eax
    sub eax, eax
    M_ChannelOut(0)
    M_ChannelOut(1)
    M_ChannelOut(2)
    M_ChannelOut(3)
    mov [rdi], ax                             ; write samples
    CompilerIf #MONO_RENDER
      add rdi, 2
    CompilerElse
      mov [rdi + 2], ax
      add rdi, 4
    CompilerEndIf
    sub ebx ,1
    !jnz .rloop
    pop rdi
    pop rbx
    !.exit:
  EndProcedure
  
  ;- Other procedures
  
  Procedure Clock(freq.l)
    !mov eax, [p.v_freq]
    !and eax, 0x003fffff
    !cdq
    !mov ecx, 0x01000000
    !mul ecx
    !mov ecx, 16 * 44100
    !div ecx
    mov rdx, SN76489_State
    mov [rdx + 4], eax
  EndProcedure
  
  Procedure Reset()
    FillMemory(@SN76489_State\vol[0], 4, 15)  ; reset volume
    FillMemory(@SN76489_State\div[0], 32)     ; clear rest of state
    SN76489_State\noiseLFSR = $f037
    SN76489_State\ticksCount = SN76489_State\ticksPerSample
  EndProcedure
  
  Procedure SN76489()
    Protected i.i
    With SN76489_State
      \vt[0] = 8191 : \vt[1] = 6506 : \vt[2] = 5168
      \vt[3] = 4105 : \vt[4] = 3261 : \vt[5] = 2590
      \vt[6] = 2057 : \vt[7] = 1634 : \vt[8] = 1298
      \vt[9] = 1031 : \vt[10] = 819 : \vt[11] = 651
      \vt[12] = 517 : \vt[13] = 411 : \vt[14] = 326
      \vt[15] = 0
      For i = 0 To 15 : \vt[i + 16] = -0.33 * \vt[i] : Next
    EndWith
    Clock(3579545)
    Reset()
  EndProcedure
  
  Procedure Write(val.a, chip.i=0)  ; will need some splite with this chip param
    Protected.u chan, cdiv, div, vol
    If val & $80
      chan = val >> 5 & 3
      cdiv = SN76489_State\div[chan] & $fff0 | (val & $f)
      SN76489_State\latchedChan = chan
      SN76489_State\latchedVol = val >> 4 & 1
    Else
      chan = SN76489_State\latchedChan
      cdiv = SN76489_State\div[chan] & $f | (val << 4 & $3f0)
    EndIf
    If SN76489_State\latchedVol
      SN76489_State\vol[chan] = SN76489_State\vol[chan] & $10 | (val & $f)
    Else
      SN76489_State\div[chan] = cdiv
      If chan = 3
        SN76489_State\noiseTap = cdiv << 1 & 8 + 1
        SN76489_State\noiseLFSR = $8000      
      EndIf
    EndIf
  EndProcedure
  
EndModule

UseModule SN76489
SN76489()

#PSG_BUFFERS = 4
Global Dim wavebuf.w(#PSG_BUFFERS - 1, 1023)
Global Dim wavehdr.WAVEHDR(#PSG_BUFFERS - 1)
Global.i PSG_Unit, WMME_Playing

DataSection
  PCM16BitSigned:
  Data.w 1,1,$AC44,0,$5888,1,2,16,0
EndDataSection  

Procedure WMME_Callback(hwo, uMsg, dwInstance, *wavehdr.WAVEHDR, dwParam2)
  If WMME_Playing And uMSG = #WOM_DONE
    Render(*wavehdr\lpData, 1024)
    waveOutWrite_(hwo, *wavehdr, SizeOf(WAVEHDR))
  EndIf
EndProcedure

Procedure PSG_Terminate()
  Protected i.i
  If PSG_Unit
    WMME_Playing = #False
    waveOutReset_(PSG_Unit)
    While i < #PSG_BUFFERS     
      waveOutUnprepareHeader_(PSG_Unit, @wavehdr(i), SizeOf(WAVEHDR))
      i + 1
    Wend        
    waveOutClose_(PSG_Unit)
    PSG_Unit = 0
  EndIf
EndProcedure  

Procedure PSG_Init()
  If waveOutOpen_(@PSG_Unit, #WAVE_MAPPER, ?PCM16BitSigned, @WMME_Callback(), 0, #CALLBACK_FUNCTION)
    MessageRequester("Error", "Unable to initialize PSG")
  Else
    While i < #PSG_BUFFERS
      wavehdr(i)\lpData = @wavebuf(i, 0)
      wavehdr(i)\dwBufferLength = 2048
      waveOutPrepareHeader_(PSG_Unit, @wavehdr(i), SizeOf(WAVEHDR))
      waveOutWrite_(PSG_Unit, @wavehdr(i), SizeOf(WAVEHDR))
      i + 1
    Wend
    WMME_Playing = #True   
  EndIf
EndProcedure

PSG_Init()

Structure VGMFSt
  type.i
  reg.a
  val.a
  pause.u
  samplenum.a
  sampleadress.i
  samplesize.i
  summofpauses.i
EndStructure
Global Dim VGMARR.VGMFSt(0)

Global TormozFlag = 1
Global PlThr

;{
Macro SetBit(Var, Bit)
  Var | (Bit)
EndMacro
 
Macro ClearBit(Var, Bit)
  Var & (~(Bit))
EndMacro

Macro TestBit(Var, Bit)
  Bool(Var & (Bit))
EndMacro
 
Macro NumToBit(Num)
  (1<<(Num))
EndMacro

Macro GetBits(Var, StartPos, EndPos)
  ((Var>>(StartPos))&(NumToBit((EndPos)-(StartPos)+1)-1))
EndMacro
;}

Procedure Play(*Value)
  
  PlayedTicks.i = 0
  PlayedUS.i = 0
  CurrentUS.i = 0
  StartMS.i = ElapsedMilliseconds()

  For i = start To ArraySize(VGMARR())-1

    Select VGMARR(i)\type
      Case 3 ; pauses    
        PlayedTicks + VGMARR(i)\pause
        PlayedUS = PlayedTicks * 22.675736961;90.702947844;22.675736961
        While (CurrentUS < PlayedUS)
          Delay(1)
          CurrentUS.i = (ElapsedMilliseconds() - StartMS) * 1000
        Wend

      Case 5 ; PSG
        Write(VGMARR(i)\val)
        
      Case 6 ; PSG
        ;Write(VGMARR(i)\val) ;Write2?
        Write(VGMARR(i)\val, 1)

    EndSelect
    
    If TormozFlag      
      Break
    EndIf 
    
  Next
  
  ;silence when stop
  Write(%10011111)
  Write(%10111111)
  Write(%11011111)
  Write(%11111111)
  
EndProcedure

Procedure ParsePlay(*FileMem)
  
  memsize = MemorySize(*FileMem)

  ;get version of vgm
  ver$ = ""
  tmp = PeekA(*FileMem + 11)
  ver$ = Str(GetBits(tmp, 4, 7))
  ver$ + Str(GetBits(tmp, 0, 3))
  tmp = PeekA(*FileMem + 10)
  ver$ + Str(GetBits(tmp, 4, 7))
  ver$ + Str(GetBits(tmp, 0, 3))
  tmp = PeekA(*FileMem + 9)
  ver$ + Str(GetBits(tmp, 4, 7))
  ver$ + Str(GetBits(tmp, 0, 3))
  tmp = PeekA(*FileMem + 8)
  ver$ + Str(GetBits(tmp, 4, 7))
  ver$ + Str(GetBits(tmp, 0, 3))

  If Val(ver$) < 151
    ot = *FileMem + 64      ; 64 - it is vgm header. no need it yet
  Else
    ot = *FileMem + 256
  EndIf  
  do = *FileMem + memsize

  ; create array with size as filesize. array will get less size, than file, but it is ok
  Dim VGMARR(memsize)

  Number.a = 0
  PSGvalue.a
  Arrayind = 0  
  
  For i = ot To do
    
    Number = PeekA(i)
    
    Select Number      
        
      Case $67  ;  0x67 0x66 tt ss ss ss ss (data)
        ; it is big wav data block        
        ; get size of wav data block
        WavDataSize = PeekI(i + 3)
        ; get adress of wav data block
        WavAddres = i + 7
        ; jump to end of wav data block
        i + 6 + WavDataSize 
        
      Case $52 ; $52 - register +0
        i+2
        
      Case $53 ; $53 - register +256
        i+2 
        
      Case $61 ; $61 - can range from 0 to 65535 (approx 1.49 seconds)
        VGMARR(Arrayind)\type = 3
        VGMARR(Arrayind)\pause = PeekU(i + 1)
        If flagpausehunt
          VGMARR(numforsizecount)\summofpauses + VGMARR(Arrayind)\pause
          flagpausehunt = 0
        EndIf
        Arrayind + 1
        i+2
        
      Case $70 To $7F ; wait n+1 samples, n can range from 0 to 15.
        VGMARR(Arrayind)\type = 3
        VGMARR(Arrayind)\pause = ((Number - $70) + 1)
        If flagpausehunt
          VGMARR(numforsizecount)\summofpauses + VGMARR(Arrayind)\pause
          flagpausehunt = 0
        EndIf
        Arrayind + 1
        
      Case $E0 ; 0xE0	dddddddd	seek to offset dddddddd (Intel byte order) in PCM data bank
        ;jump far
        i + 4
        
      Case $80 To $8F
        flagpausehunt = 1        
        If Number > $80
          VGMARR(Arrayind)\type = 3
          VGMARR(Arrayind)\pause = (Number - $80)
          VGMARR(numforsizecount)\summofpauses + VGMARR(Arrayind)\pause
          Arrayind + 1
        EndIf
        
      Case $4F ; 0x4F	dd	Game Gear PSG stereo, write dd to port 0x06 ; dune with samples
        ; ignore, just jump far
        i + 1
        
      ;MAIN COMMAND :))
      Case $50 ; 0x50	dd	PSG (SN76489/SN76496) write value dd ; dune with samples
        i + 1
        PSGvalue = PeekA(i)
        VGMARR(Arrayind)\type = 5
        VGMARR(Arrayind)\val = PSGvalue
        Arrayind + 1
        
      Case $30 ; 0x50	dd	PSG (SN76489/SN76496) write value dd ; dune with samples
        i + 1
        PSGvalue = PeekA(i)
        VGMARR(Arrayind)\type = 6
        VGMARR(Arrayind)\val = PSGvalue
        Arrayind + 1  
        
      Case $62 ; wait 735 samples (60th of a second), a shortcut for 0x61 0xdf 0x02 ; Lego Tune
        VGMARR(Arrayind)\type = 3
        VGMARR(Arrayind)\pause = 735
        If flagpausehunt
          VGMARR(numforsizecount)\summofpauses + VGMARR(Arrayind)\pause
          flagpausehunt = 0
        EndIf
        Arrayind + 1
        
      Case $63 ; wait 882 samples (50th of a second), a shortcut For 0x61 0x72 0x03
        VGMARR(Arrayind)\type = 3
        VGMARR(Arrayind)\pause = 882
        If flagpausehunt
          VGMARR(numforsizecount)\summofpauses + VGMARR(Arrayind)\pause
          flagpausehunt = 0
        EndIf
        Arrayind + 1
        
      Case $66 ; end of sound
        Debug "end"        
        Break 
        
      Default 
        Debug Hex(Number)
        
        
    EndSelect
    

  Next
  
  Play(0) 
  
EndProcedure

If OpenWindow(#Window, 100, 200, 195, 260, "PureBasic Window", #PB_Window_SystemMenu | #PB_Window_MinimizeGadget | #PB_Window_MaximizeGadget)
  
  StringGadget(#PathString, 10, 20, 120, 20, "", #PB_String_ReadOnly)
  ButtonGadget(#OpenButton, 130, 20, 50, 20, "open")
  
  TextGadget(#Text, 10, 40, 170, 120, "WARNING! VGZ must be unpacked by manualy into VGM (7z can do this. i cant plug zlib yet). and see what chips was used by this VGM. it plays only one SN76489 (not dual). YM2612 is ignored. any VGM with other chips can crash programm.")

  Repeat
    
    Event = WaitWindowEvent()
    
    Select Event

      Case #PB_Event_Gadget
        Select EventGadget()
          ;- Event  
          Case #OpenButton
            If File$
              StandardFile$ = GetPathPart(File$)
            Else
              StandardFile$ = "C:\"        ; initial path + file
            EndIf
            Pattern$ = "VGM files (*.vgm)|*.vgm;";*.vgz|"   ; set first pattern  (index = 0)
            Pattern = 0    ; use the second of the five possible patterns as standard

            ; Now we open a filerequester, you can change the pattern and will get the index after closing
            File$ = OpenFileRequester("Please choose file to load", StandardFile$, Pattern$, Pattern)
            If File$
              SetGadgetText(#PathString, File$)
              
              If ReadFile(#File, File$)
                length = Lof(#File)                            ; get the length of opened file
                If *MemoryID
                  FreeMemory(*MemoryID)
                  *MemoryID = 0
                EndIf
                *MemoryID = AllocateMemory(length)         ; allocate the needed memory
                If *MemoryID
                  bytes = ReadData(#File, *MemoryID, length)   ; read all data into the memory block
                EndIf
                CloseFile(#File)
                
                TormozFlag = 0
                
                PlThr = CreateThread(@ParsePlay(), *MemoryID)
                
                
              EndIf

              
            EndIf
            
          EndSelect
            
            

      Case #PB_Event_CloseWindow
         Quit = 1
         
    EndSelect

  Until Quit = 1
  
EndIf

End 
but by theory stereo is play by one chip. not dual. just this stereo is play same for both channel. this x4F can just turn off left side for 2 channel for example. right channel will play same, just mute. dual chips need not for stereo, but for bigger channels. for more complex sound, more track for track.
Post Reply