Software Simulation of ATARI ST Sound Chip YM 2149 (aka AY-3-8910)

Started by Midimaster, October 12, 2023, 08:41:48

Previous topic - Next topic

Midimaster

This tutorial will show how to write a software emulator of a given TTL-Chip. In our case it will be the YM 2149, also know as AY-3-8910. One of the favorite sound chips of the Eighties, used in a lot of computers and game consoles. We have the original manual of the developer with all the description and sketches, plans, etc...:

http://users.rcn.com/carlott/ay3-8910.pdf

The language will be BlitzMax NG and the Audio Output will be mima.miniaudio.mod, where we can reach latencies below 20msec. The Emulation can theoretically also be done with the trad. TAudio-approach, but then you have to make some concession.

The base: MiniAudio-Device

We start with the PlayNoise.bmx example and adapt it to a TYPE-Structure. The quality of the MiniAudio-Device is much too good compared to the old 4bit-Audio-Out of the ATARI ST, but later we will learn how to reduce the quality.

This first step defines an audio out and fills it with NOISE:

SuperStrict
Import mima.miniaudio
Graphics 800,600

YM.Start
Repeat
    Cls
    DrawText "click LEFT MOUSE for NOISE",100,100
    If MouseDown(1)
        YM.Switch YM.ON
    Else
        YM.Switch YM.OFF
    EndIf
    Flip 0
Until AppTerminate()
YM.Stop
End

Type YM
    Global MiniAudio:TMiniAudio=New TMiniAudio
    Global OnOff:Int
    Const SAMPLE_RATE:Int = 48000
    Const MONO:Int=1, ON:Int=1, OFF:Int=0


    Function Start()
        MiniAudio.OpenDevice( MiniAudio.PLAYBACK, Miniaudio.FORMAT_S16, MONO, SAMPLE_RATE, MyCallBack)   
        MiniAudio.StartDevice()
    End Function
   
    Function Stop()
        Miniaudio.CloseDevice()
    End Function

    Function MyCallBack(void:Byte Ptr, PlayBuffer:Short Ptr, RecordingBuffer:Short Ptr, Frames:Int)
        If ONOff=ON
            For Local i:Int=0 Until Frames
                PlayBuffer[i] = Rand(-10000,+10000)
            Next
        EndIf
    End Function
   
    Function Switch(State:Int)
        OnOff = State
    End Function
End Type

The MiniAudio asks for 48000 samples each second. This is done in a callback, which reach us every 10msec, so we have to fill the buffer which 480 data each time. The range of the DATA can reach from -22000 until +22000 (SIGNED SHORT -3dB).

Later we will expand this to STEREO, because the YM 2149 was able to send Generators to LEFT or RIGHT.


...back from Egypt

Midimaster

The Tone-Generators

A WAVE is a form that continously repeats itself after a period of time. After a ½-period of positiv values, it followes the same shape for a  ½-period with negativ values.

The YM 2149 is able to generate SQUARE sound. So we need to add these generator type to calculate the sample value before sending all to the MiniAudio.

A SQUARE generator only knows three states: -1 and 0 and +1

To build a signal the generator stays for a ½-period on +1 then goes via 0 to the new state -1 for the same time, then back via 0 to +1, etc...

As shorter the period is as higher is the tone. In our example a period of 480 steps would result in a 100Hz tone, because 48000/480=100Hz.
A second example: a period length of 30 steps will result in a tone of 48000/30=1600Hz.

So our TGenerator need (at the moment) four parameter

Form      to know the wave form SQUARE (and later also NOISE)
Period    to keep the ½-period length
Counter   counts down from Period to zero
State     -1 or +1 for output

As one waveform cuts two times the zero-line we need the half period length to react in the middle of the wave

and we need a function which defines what happens each step:

Method OneStep:Int()
'returns the sample value of this generator
    If Counter=0
        Counter = Period
        State   = - State
        Return 0
    EndIf
    Counter:-1
    Return State
End Method

In case of Couter=0, the counter is refilled and the State swaps from -1 to +1. In all other cases the State is returned as sample value.

This pratice means also, that a change of Period will only take effect when Counter becomes 0


In the CallBack we now step 48000 times per second into the Generator, which returns (as result) the sample value:

    Function MyCallBack(void:Byte Ptr, PlayBuffer:Short Ptr, RecordingBuffer:Short Ptr, Frames:Int)
        If OnOff=ON
            For Local i:Int=0 Until Frames
                PlayBuffer[i] = Generator[1].OneStep()*10000
            Next
        EndIf
    End Function
The 10000 can bee seen as something like Total-Volume


Her is the runable complete example:

SuperStrict
Import mima.miniaudio
Graphics 800,600

YM.Start
YM.SetPeriod 1,480

Repeat
    Cls
    DrawText "click LEFT MOUSE for SQUARE",100,100
    If MouseDown(1)
        YM.Switch YM.ON
    Else
        YM.Switch YM.OFF
    EndIf
    Flip 0
Until AppTerminate()
YM.Stop
End

Type YM
    Global MiniAudio:TMiniAudio=New TMiniAudio
    Global OnOff:Int
    Global Generator:TGenerator[5]
   
    Const SAMPLE_RATE:Int = 48000
    Const MONO:Int=1, ON:Int=1, OFF:Int=0

   
    Function Start()
        MiniAudio.OpenDevice( MiniAudio.PLAYBACK, Miniaudio.FORMAT_S16, MONO, SAMPLE_RATE, MyCallBack)   
        MiniAudio.StartDevice()
        Generator[1]= TGenerator.Create(TGenerator.SQUARE)
    End Function
   
    Function Stop()
        Miniaudio.CloseDevice()
    End Function

    Function MyCallBack(void:Byte Ptr, PlayBuffer:Short Ptr, RecordingBuffer:Short Ptr, Frames:Int)
        If OnOff=ON
            For Local i:Int=0 Until Frames
                PlayBuffer[i] = Generator[1].OneStep()*10000
            Next
        EndIf
    End Function
   
    Function Switch(State:Int)
        OnOff = State
    End Function
   
    Function SetPeriod(Nr:Int,Period:Int)
        Generator[1].Period = Period/2
    End Function
End Type
 

Type TGenerator
    Const SQUARE:Int=1, NOISE:Int=2

    Field Form:Int, Period:Int, Counter:Int, State:Int 

    Function Create:TGenerator(Form:Int)
        Local loc:TGenerator = New TGenerator
        loc.Form  = Form
        loc.State = 1
        Return loc
    End Function

    Method OneStep:Int()
    'returns the sample value of this generator
        If Counter=0
            Counter = Period
            State   = - State
            Return 0
        EndIf
        Counter:-1
        Return State
    End Method
End Type



 
...back from Egypt

Midimaster

Convert YM-periods to 48kHz-periods

Now we add the first two registers of the YM-2149. Each generator has two register where we can set the Frequency the generator should produce. To get a fine resolution we need two 8-Bit registers. One HIGH for the rough resultion (4bits used) and one LOW for the fine resolution (8bits used). Together this makes 12bits and 4096 possible frequency values.

Depending on the PROCESSOR CLOCK rate the YM-2149 produces a certain HERTZ tone. This means, that the same music file on several vintage computers leaded to different keys (melody altidute). So the developers need to write the same file new for each machine.

In our emulator we use the PROCESS_CLOCK= 1.7MHz from the chip manual, but you can set it to any other clock speed.

The Registers of the YM-2149 do ot need the HERTZ but the PERIOD LENGTH of the desired wave. So first step is to add a developer support function to calculate the two values HIGH and LOW for a given frequency:

If KeyHit(KEY_F)
    Local High:Int, Low:Int
    YM.AskPeriod 440, High, Low
    Print "to get 440Hz you have to set HIGH=" + High + "  and LOW=" + low   
EndIf
...
Function AskPeriod(Hertz:Int, High:Int Var, Low:Int Var)
    'informs about how to fill the both register to hear a certain frequency
    Hertz = PROCESS_CLOCK /16 / Hertz
    High  = Hertz /256
    Low  = Hertz Mod 256
End Function

Remember, that the HIGH register can only accept values between 0 and 15 (only 4bits used), means that Frequencies below 28Hz are not possible



The registers of the YM-2149 where not accessible from outside the chip (More about this later...) But during the development we open it with a function:

Function SetRegister(Adress:Int, Value:Int)
    Register[Adress]=Value       
    Select Adress
        Case 0,1
            SetPeriod 1, Register[0]*256+Register[1]
    End Select
End Function
The two tone registers for Generator A have the adress 0 and 1 and we immediately calculate the resulting real period.
This calculation should be clear: The function combines both registers to a 12bit value


This is more complex:

Function SetPeriod(Nr:Int,Period:Int)
    If Period=0
        Generator[1].Period=0
    Else
        Print "Set  HERTZ=" + PROCESS_CLOCK /16 /Period  + "  ½-PERIOD=" + (Double(SAMPLE_RATE*Period*8)/PROCESS_CLOCK)           
        Generator[1].Period  = (Double(SAMPLE_RATE*Period*8)/PROCESS_CLOCK)
    EndIf
End Function

We have to think about two formulas:

First is how to come from Register to Real-HERTZ:
HERTZ = PROCESS_CLOCK / 16 / Period
This is how the YM-2149 calculated this

Second is how to come from HERTZ now to 48kHzAudio-Device:
½-PERIOD = SAMPLE_RATE / HERTZ / 2
The division by 2 is because we need the length of a half period for flipping

The combination gives:

½-PERIOD = SAMPLE_RATE * Period * 8 / PROCESS_CLOCK


ok...step-by-step:
½-PERIOD = SAMPLE_RATE  /  HERTZ  /  2
.
.
½-PERIOD = SAMPLE_RATE  /  (PROCESS_CLOCK /16 /Period)  /  2
.
.
           SAMPLE_RATE    PROCESS_CLOCK      2
½-PERIOD = ----------- /  -------------  /  ---
                1          16 * Period       1

 
           SAMPLE_RATE      16 * Period      1
½-PERIOD = ----------- *  -------------  *  ---
                1         PROCESS_CLOCK      2


            SAMPLE_RATE * Period * 16
½-PERIOD = --------------------------
                PROCESS_CLOCK    * 2


½-PERIOD =(SAMPLE_RATE*Period*8)/PROCESS_CLOCK)


and now we need to update OneStep() where the Counter is now based on DOUBLE:

Method OneStep:Int()
    Global RealCounts:Int
    'returns the sample value of this generator
    If Counter<0.5
        Counter:+ Period
        Print "counter" + Counter  + " " + RealCounts
        RealCounts=0
        State  = - State
        Return 0
    EndIf
    RealCounts:+ 1
    Counter:-1
    Return State
End Method

I add for demonstration purpose a PRINT line, which show how the period length now alters



Here is the complete runable code. You can play around with the frequency by moving the mouse now:

SuperStrict
Import mima.miniaudio
Graphics 800,600

YM.Start
YM.SetRegister 0, 1
YM.SetRegister 1, 123

Local Mouse:Int
Repeat
    Cls
    DrawText "click LEFT MOUSE for SQUARE",100,100
    DrawText "move the MOUSE-X to change frequency",100,140
    DrawText "press F to aks for frequency 555Hz ----> PRINT ",100,180
    If MouseDown(1)
        YM.Switch YM.ON
    Else
        YM.Switch YM.OFF
    EndIf
   
    If KeyHit(KEY_F)
        Local High:Int, Low:Int
        YM.AskPeriod 555, High, Low
        Print "to get 555Hz you have to set HIGH=" + High + "  and LOW=" + low   
    EndIf
   
    If MouseX()<> Mouse
        Mouse = MouseX()
        If Mouse<0 Then Continue
        YM.SetRegister 0, (Mouse *4) /256
        YM.SetRegister 1, (Mouse *4) Mod 256
       
    EndIf
    Flip 0
Until AppTerminate()
YM.Stop
End

Type YM
    Global MiniAudio:TMiniAudio=New TMiniAudio
    Global OnOff:Int
    Global Generator:TGenerator[5]
    Global Register:Int[16]
    Const PROCESS_CLOCK:Int= 1789770   
    Const SAMPLE_RATE:Int = 48000
    Const MONO:Int=1, ON:Int=1, OFF:Int=0
   
    Function Start()
        MiniAudio.OpenDevice( MiniAudio.PLAYBACK, Miniaudio.FORMAT_S16, MONO, SAMPLE_RATE, MyCallBack)   
        MiniAudio.StartDevice()
        Generator[1]= TGenerator.Create(TGenerator.SQUARE)
    End Function
   
    Function Stop()
        Miniaudio.CloseDevice()
    End Function

    Function MyCallBack(void:Byte Ptr, PlayBuffer:Short Ptr, RecordingBuffer:Short Ptr, Frames:Int)
        If OnOff=ON
            For Local i:Int=0 Until Frames
                PlayBuffer[i] = Generator[1].OneStep()*10000
            Next
        EndIf
    End Function
   
    Function Switch(State:Int)
        OnOff = State
    End Function
   
    Function SetRegister(Adress:Int, Value:Int)
        Register[Adress]=Value       
        Select Adress
            Case 0,1
                SetPeriod 1, Register[0]*256+Register[1]
        End Select
    End Function
   
    Function SetPeriod(Nr:Int,Period:Int)
        If Period=0
            Generator[1].Period=0
        Else
            Print "Set  HERTZ=" + PROCESS_CLOCK /16 /Period  + "  HALF-PERIOD=" + (Double(SAMPLE_RATE*Period*8)/PROCESS_CLOCK)           
            Generator[1].Period  = (Double(SAMPLE_RATE*Period*8)/PROCESS_CLOCK)
        EndIf
    End Function
   
    Function AskPeriod(Hertz:Int, High:Int Var, Low:Int Var)
        'informs about how to fill the both register to hear a certain frequency
        Hertz = PROCESS_CLOCK /16 / Hertz
        High  = Hertz /256
        Low  = Hertz Mod 256
    End Function
End Type
 

Type TGenerator
    Const SQUARE:Int=1, NOISE:Int=2

    Field Form:Int, Period:Double, Counter:Double, State:Int 

    Function Create:TGenerator(Form:Int)
        Local loc:TGenerator = New TGenerator
        loc.Form  = Form
        loc.State = 1
        Return loc
    End Function

    Method OneStep:Int()
    Global RealCounts:Int
    'returns the sample value of this generator
        If Counter<0.5
            Counter:+ Period
            Print "counter" + Counter  + " " + RealCounts
            RealCounts=0
            State  = - State
            Return 0
        EndIf
        RealCounts:+ 1
        Counter:-1
        Return State
    End Method
End Type
...back from Egypt

Midimaster

The Noise Generator

Register 6 is a NOISE register. We can put in values between 0 and 31 (5-bit) and this will drive Generator 4 to produce a

Frequency Modulated Pseudo Random Pulse Width Square Wave Output


Häää? What...

This is really future stuff. Today this technique is again up-to-date, because Electro-Cars-Developers try to find a nice sounding car-Noise and reanimate this way to produce random sound.

First of all. This is much better than to output a simply Rand(-X, +X) to a Sounddevice, because the PWM-Noise enables "colored" noise going from deep rumble upto high blowing.

The noise generator produces again simple SQUARE signals but the period length changes by random. This means each time when the ZERO-cross is reached the Counter is not refilled with Period, but with Rand(2,Period)

Method OneStep:Int()
    'returns the sample value of this generator
    If Counter<0.5
        If Form=SQUARE
            Counter:+ Period           
        Else 'means: Form=NOISE
            Counter = Rand(2,Period)           
        EndIf
        State   = - State
        Return 0
    EndIf
    Counter:-1
    Return State
End Method


We do not exactly know, how the YM-2149 uses the register 6 to produce a PWM-Noise. The manual says it simply uses the value 1 to 31 as random maximum. But I did some experiments and must say, that this produces very "high" noise. So I multiplied this input by 16 to get "longer" periods and so deeper noise. Try it out yourself:

"document" approach
Function SetRegister(Adress:Int, Value:Int)
    ...       
    Select Adress
        ...
        Case 6
            ' noise frequency control
            SetPeriod 4, (Register[6] & %11111) * 2 ' to compensate my period-calculation        


my approach
Function SetRegister(Adress:Int, Value:Int)
    ...       
    Select Adress
        ...
        Case 6
            ' noise frequency control
            '                                  ' || * 16 is experimental... sounds better
            '                                    \/ 
            SetPeriod 4, (Register[6] & %11111)  * 16   


to find out what is right or wrong, we would need a vintage machine to compare the sounds at the speakers.


Here is the runable complete code for NOISE:

SuperStrict
Import mima.miniaudio
Graphics 800,600

YM.Start
YM.SetRegister 0, 1
YM.SetRegister 1, 123
YM.SetRegister 6, 15

Local XMouse:Int, YMouse:Int

Repeat
    Cls
    DrawText "click LEFT MOUSE for NOISE",100,100
    DrawText "move the MOUSE-X to change noise frequency",100,140
    If MouseDown(1)
        YM.Switch YM.ON
    Else
        YM.Switch YM.OFF
    EndIf
   
    If KeyHit(KEY_F)
        Local Search:Int=400, High:Int, Low:Int
        YM.AskPeriod Search, High, Low
        Print "to get " + Search + "Hz you have To set HIGH=" + High + "  And LOW=" + low   
    EndIf
   
    If MouseX()<> XMouse
        XMouse = MouseX()
        If XMouse<0 Then Continue
         YM.SetRegister 6, XMouse / 25       
    EndIf
    Flip 1
Until AppTerminate()
YM.Stop
End

Type YM
    Global MiniAudio:TMiniAudio=New TMiniAudio
    Global OnOff:Int
    Global Generator:TGenerator[5]
    Global Register:Int[16]
    Const PROCESS_CLOCK:Int= 1789770   
    Const SAMPLE_RATE:Int = 48000
    Const MONO:Int=1, ON:Int=1, OFF:Int=0


' public ***********************************************   
    Function Start()
        MiniAudio.OpenDevice( MiniAudio.PLAYBACK, Miniaudio.FORMAT_S16, MONO, SAMPLE_RATE, MyCallBack)   
        MiniAudio.StartDevice()
        Generator[1]= TGenerator.Create(TGenerator.SQUARE)
        Generator[4]= TGenerator.Create(TGenerator.NOISE)
    End Function

   
    Function Stop()
        Miniaudio.CloseDevice()
    End Function

   
    Function AskPeriod(Hertz:Int, High:Int Var, Low:Int Var)
        'informs about how to fill the both register to hear a certain frequency
        Hertz = PROCESS_CLOCK /16 / Hertz
        High  = Hertz /256
        Low   = Hertz Mod 256
    End Function
   


   
' developer ***********************************************

    Function SetRegister(Adress:Int, Value:Int)
        Register[Adress]=Value       
        Select Adress
            Case 0,1
                ' tone frequency control
                SetPeriod 1, Register[0]*256+Register[1]
            Case 6
                ' noise frequency control
                '                                  ' || * 16 is experimental... sounds better
                '                                    \/ 
                SetPeriod 4, (Register[6] & %11111)  * 16               
        End Select
    End Function

   
    Function Switch(State:Int)
        OnOff = State
    End Function




' private ***********************************************

    Function MyCallBack(void:Byte Ptr, PlayBuffer:Short Ptr, RecordingBuffer:Short Ptr, Frames:Int)
        If OnOff=ON
            For Local i:Int=0 Until Frames
                PlayBuffer[i] = Generator[4].OneStep() * 10000
            Next
        EndIf
    End Function

   
    Function SetPeriod(Nr:Int,Period:Int)
        If Period=0
            Generator[4].Period=0
        Else
            Print "Set  HERTZ=" + PROCESS_CLOCK /16 /Period  + "   HALF-PERIOD=" + (Double(SAMPLE_RATE*Period*8)/PROCESS_CLOCK)           
            Generator[4].Period  = (Double(SAMPLE_RATE*Period*8)/PROCESS_CLOCK)
        EndIf
    End Function
   
End Type
 

Type TGenerator
    Const SQUARE:Int=1, NOISE:Int=2

    Field Form:Int, Period:Double, Counter:Double, State:Int
   
   
    Function Create:TGenerator(Form:Int)
        Local loc:TGenerator = New TGenerator
        loc.Form  = Form
        loc.State = 1
        Return loc
    End Function


    Method OneStep:Int()
        'returns the sample value of this generator
        If Counter<0.5
            If Form=SQUARE
                Counter:+ Period           
            Else 'means: Form=NOISE
                Counter = Rand(2,Period)           
            EndIf
            State   = - State
            Return 0
        EndIf
        Counter:-1
        Return State
    End Method
End Type



...back from Egypt

Midimaster

Volume Control

Each Of the three Generators has an individual volume control of 16 leves. This is driven by an extra register. The value of the register is converted into a electrique voltage between 0.0Volt and ±1.0Volt. The scale is not linear, but logarithmic. The manual does not descripe how exactly the Volt needs to be caclulated but a figure shows that 15=1.00V and 14= 0.70V and 13=0.50V.

So I use the formula
Volt= 0.7(15-Value)

The Class will offer developers a possiblity to calculate this completly new:

Type YM
Global VolTable:Int[16]
...
Function Start()
BuildVolumeTable 0.7 , 10000
.....
       
Function BuildVolumeTable(faktor:Double, Multiplicator:Int)
For Local i:Int= 1 To 15
VolTable[i] = faktor^(15-i)*Multiplicator
Next
End Function )

Here you can select the curve form with the parameter faktor and the Audio-Out related Multiplicator to reach maximum Volume of your Device. In our case 10000.



In the CallBack we calculate the SAMPLE now with a product of Generator-State and Volume:

Function SetRegister(Adress:Int, Value:Int)
Register[Adress]=Value
Select Adress
...

Case 10
' volume control
SetVolume 1, Register[10]  & %1111
...
End Function

Function SetVolume(Nr:Int,Volume:Int)
OutVolume[1] = VolTable[Volume]
Print "Volume now Table=" + Volume + "     real=" + OutVolume[1]
End Function

Function MyCallBack(void:Byte Ptr, PlayBuffer:Short Ptr, RecordingBuffer:Short Ptr, Frames:Int)
If OnOff=ON
For Local i:Int=0 Until Frames
PlayBuffer[i] = Generator[1].OneStep() * OutVolume[1]
Next
EndIf
End Function

This is how the document describes the calculation. As musician I know, that this leads to crackles, because the horizontal line of a SQUARE will immediately get a "step", when you change the volume during a ON-STATE. This happens at any time when the Generator is on. Better is to wait for the moment, when the Generator makes a ZERO-cross ( and is mute in this moment). Here we can change the volume with a better audio experience. So I added:

Function MyCallBack(void:Byte Ptr, PlayBuffer:Short Ptr, RecordingBuffer:Short Ptr, Frames:Int)
If OnOff=ON
For Local i:Int=0 Until Frames
' this is volume change WITHOUT waiting for ZERO-cross:
' feature or bug?
' PlayBuffer[i] = Generator[1].OneStep() * OutVolume[1]

' this is experimental volume change WITH waiting for ZERO-cross:
PlayBuffer[i] = Generator[1].OneStep() * Generator[1].Volume
Next
EndIf
End Function

We can not see from outside whether the CHIP perhaps handles this the same way. We only have the Register 10 and no information how it will be processed by the CHIP.


This better approach needs some change in the Generator. When we set the volume, we only set the future NextVolume, which will become the real Volume during the next Zero-cross:

Function SetVolume(Nr:Int,Volume:Int)
....
Generator[1].NextVolume  = VolTable[Volume]
End Function

Type TGenerator
    ....
Field ...., Volume:Int, NextVolume:Int
...
Method OneStep:Int()
'returns the sample value of this generator
If Counter<0.5
If Form=SQUARE
Counter:+ Period
EndIf

State   = - State
Volume  = NextVolume
Return 0
EndIf
Counter:-1
Return State
End Method


As a developer you can play around with both ideas. I keep all optimizations and basic approaches in the Class.

Here is the runable Version:

SuperStrict
Import mima.miniaudio
Graphics 800,600

YM.Start
YM.SetRegister 0, 2
YM.SetRegister 1, 123
'YM.SetRegister 6, 15

Local XMouse:Int, YMouse:Int

Repeat
Cls
DrawText "click LEFT MOUSE for SQUARE",100,100
DrawText "move the MOUSE-Y to change volume",100,180
If MouseDown(1)
YM.Switch YM.ON
Else
YM.Switch YM.OFF
EndIf

If KeyHit(KEY_F)
Local High:Int, Low:Int
YM.AskPeriod 400, High, Low
Print "to get 400Hz you have to set HIGH=" + High + "  and LOW=" + low
EndIf

If MouseY()<> YMouse
YMouse = MouseY()
If YMouse<0 Then Continue
YM.SetRegister 10, YMouse/38
EndIf
Flip 0
Until AppTerminate()
YM.Stop
End

Type YM
Global MiniAudio:TMiniAudio=New TMiniAudio
Global OnOff:Int
Global Generator:TGenerator[5]
Global Register:Int[16]
Const PROCESS_CLOCK:Int= 1789770
    Const SAMPLE_RATE:Int = 48000
    Const MONO:Int=1, ON:Int=1, OFF:Int=0

Global VolTable:Int[16]
Global OutVolume:Double[3]

' public ***********************************************
Function Start()
MiniAudio.OpenDevice( MiniAudio.PLAYBACK, Miniaudio.FORMAT_S16, MONO, SAMPLE_RATE, MyCallBack)
MiniAudio.StartDevice()
BuildVolumeTable 0.7 , 10000
Generator[1]= TGenerator.Create(TGenerator.SQUARE)
Generator[4]= TGenerator.Create(TGenerator.NOISE)
End Function


Function Stop()
Miniaudio.CloseDevice()
End Function


Function AskPeriod(Hertz:Int, High:Int Var, Low:Int Var)
'informs about how to fill the both register to hear a certain frequency
Hertz = PROCESS_CLOCK /16 / Hertz
High  = Hertz /256
Low   = Hertz Mod 256
End Function




' developer ***********************************************

Function SetRegister(Adress:Int, Value:Int)
Register[Adress]=Value
Select Adress
Case 0,1
' frequency control
SetPeriod 1, Register[0]*256+Register[1]
Case 6
' frequency control
Print "register 6 ======>"+ Register[6]
SetPeriod 4, (Register[6] & %11111)  *16

Case 10
' volume control
SetVolume 1, Register[10]  & %1111

End Select
End Function


Function Switch(State:Int)
OnOff = State
End Function




' private ***********************************************

Function MyCallBack(void:Byte Ptr, PlayBuffer:Short Ptr, RecordingBuffer:Short Ptr, Frames:Int)
If OnOff=ON
For Local i:Int=0 Until Frames
' this is volume change WITHOUT waiting for ZERO-cross:
' feature or bug?
' PlayBuffer[i] = Generator[1].OneStep() * OutVolume[1]

' this is experimental volume change WITH waiting for ZERO-cross:
PlayBuffer[i] = Generator[1].OneStep() * Generator[1].Volume
Next
EndIf
End Function


Function SetPeriod(Nr:Int,Period:Int)
If Period=0
Generator[4].Period=0
Else
Generator[1].Period  = (Double(SAMPLE_RATE*Period*8)/PROCESS_CLOCK)
EndIf
End Function


Function SetVolume(Nr:Int,Volume:Int)
OutVolume[1] = VolTable[Volume]
Print "Volume now Table=" + Volume + "     real=" + OutVolume[1]
Generator[1].NextVolume  = VolTable[Volume]
End Function


Function BuildVolumeTable(faktor:Double, Multiplicator:Int)
For Local i:Int= 1 To 15
VolTable[i] = faktor^(15-i)*Multiplicator
Next
End Function
End Type
 

Type TGenerator
    Const SQUARE:Int=1, NOISE:Int=2

Field Form:Int, Period:Double, Counter:Double, State:Int, Volume:Int, NextVolume:Int

Function Create:TGenerator(Form:Int)
Local loc:TGenerator = New TGenerator
loc.Form  = Form
loc.State = 1
Return loc
End Function

Method OneStep:Int()
'returns the sample value of this generator
If Counter<0.5
If Form=SQUARE
Counter:+ Period
Else ' NOISE
Counter = Rand(1,Period)
EndIf
State   = - State
Volume  = NextVolume
Return 0
EndIf
Counter:-1
Return State
End Method
End Type



...back from Egypt

Midimaster

And Then They Were Three

The YM-2149 has three tone generators and one noise generator.
 
Register  0   A  Tone     Period LOW   8bit
Register  1   A  Tone     Period HIGH  4bit
Register  8   A  Channel  Volume       4bit*
Register  2   B  Tone     Period LOW   8bit
Register  3   B  Tone     Period HIGH  4bit
Register  9   B  Channel  Volume       4bit*
Register  4   C  Tone     Period LOW   8bit
Register  5   C  Tone     Period HIGH  4bit
Register 10   C  Channel  Volume       4bit*
Register  6  ABC Noise    Period       5bit
(*= more bits used but not for volume)

The NOISE Generator can be added to A B and C. So the Volume Registers are called CHANNEL Volume, because they drive NOISE and TONE.

Additional we need one Register to switch ON/OFF the TONEs and the NOISEs. Yamaha calls it MIXER, but musicians would call it more a MUTE-Register

Register  7  ABC MIXER   ON/OFF       6bit*

-----------------------------------------------------
Bits:    7    6 |  5  |  4  |  3  |  2  |  1  |  0  | 
-----------------------------------------------------
           *    |      NOISE      |     TONE        |
-----------------------------------------------------
On/Off     *    |  C  |  B  |  A  |  C  |  B  |  A  |
-----------------------------------------------------


(*= more bits used but not for mixer)


ATTENTION: The Mixer bits work like MUTE: A "0" means "ON", and a "1" means OFF (=MUTE).

Example:
              -- CBA CBA
Register 7 = "00|101|001"

Channel A is OFF
Channel B is ON with TONE and NOISE
Channel C is ON with TONE


Now to our changes is the code:


now we start four generators:
Type YM
...
Const GEN_A:Int=1, GEN_B:Int=2, GEN_C:Int=3, GEN_N:Int=4
...
Function Start()
...
Generator[GEN_A]= TGenerator.Create(TGenerator.SQUARE)
Generator[GEN_B]= TGenerator.Create(TGenerator.SQUARE)
Generator[GEN_C]= TGenerator.Create(TGenerator.SQUARE)
Generator[GEN_N]= TGenerator.Create(TGenerator.NOISE)
End Function


To play all channels we need sum up all source and multiply them with the volumes:

Function MyCallBack(void:Byte Ptr, PlayBuffer:Short Ptr, RecordingBuffer:Short Ptr, Frames:Int)
For Local i:Int=0 Until Frames
PlayBuffer[i] = Mixer()
Next
End Function

Function Mixer:Int()
Local Sample:Int
'
Local NoiseSample:Int = Generator[GEN_N].OneStep()
For Local i:Int=1 To 3
Sample = Sample + (Generator[i].OneStep() * ChannelOn(i) + NoiseSample * ChannelOn(i+3)) * Generator[i].Volume
Next
Return Sample
End Function


ChannelOn() checks the bits of Register 7, whether a TONE or NOISE is not muted:

Function ChannelOn:Int(Bit:Int)
Bit = 1 Shl (bit-1)
Return Not (Register[7] & Bit)
End Function


We have the biggest changes in the function SetRegister(). Here we check the check the plausibility of the incoming data before we store them into a register:

Function SetRegister(Adress:Int, Value:Int)
Select Adress
Case 0, 2, 4
Value= Value & %11111111
Case 1, 3, 5, 13
Value= Value & %1111
Case 6, 8, 9, 10
Value= Value & %11111
Case 7, 11, 12, 14, 15
End Select

Register[Adress]=Value
...


and the we fill the generators with the corresponding real values, calculated from the registers:

Select Adress
Case 0,1,2,3,4,5
' frequency control
Adress = Adress/2
SetPeriod Adress+1, Register[2*Adress+1]*256+Register[2*Adress]
Case 6
' noise frequency control
SetPeriod GEN_N, Register[6] *16   ' sound better then *2
Case 7
' mixer on/off control
Print "register 7 ======>"+ Register[7]
Case 8,9,10
' volume control
SetVolume Adress-7, Register[Adress]  & %1111

End Select



This time we need a real big test enviroment to give the developer a chance to play along with the parameters. This is not part of the YM-2149-class, but already something like a end-user app. Therefore I wrote for you also a minimal-GUI. In the end the Class has only 200 code-lines and the "main app" adds another 200 lines now:

TestEnviro_1.gif



This is the complete runable code:

SuperStrict
Import mima.miniaudio
AppTitle = "YM-2149 Emulator  www.midimaster.de"
Graphics 800,400
SetClsColor 1,77,55
Global Gadget:TGui[16]

YM.Start
YM.SetRegister 0, 0
YM.SetRegister 1, 9
YM.SetRegister 2, 0
YM.SetRegister 3, 9
YM.SetRegister 4, 0
YM.SetRegister 5, 9
YM.SetRegister 8, 13
YM.SetRegister 9, 13
YM.SetRegister 10, 13
YM.SetRegister 6, 29
YM.SetRegister 7, %111111

DefineDisplay

Repeat
Cls
TGui.DrawAll
CheckGUI
Flip
Until AppTerminate()
YM.Stop
End



'*******************************************************************
' THE YM149 CHIP:
'*******************************************************************

Type YM
Const PROCESS_CLOCK:Int= 1789770
    Const SAMPLE_RATE:Int = 48000
    Const MONO:Int=1, ON:Int=1, OFF:Int=0
    ' Generator Names:
Const GEN_A:Int=1, GEN_B:Int=2, GEN_C:Int=3, GEN_N:Int=4

Global MiniAudio:TMiniAudio=New TMiniAudio
Global Generator:TGenerator[5]
Global Register:Int[16]

Global VolTable:Int[16]
Global OutVolume:Double[4]


' public functions ***********************************************
Function Start()
MiniAudio.OpenDevice( MiniAudio.PLAYBACK, Miniaudio.FORMAT_S16, MONO, SAMPLE_RATE, MyCallBack)
MiniAudio.StartDevice()
BuildVolumeTable 0.7 , 10000
Generator[GEN_A]= TGenerator.Create(TGenerator.SQUARE)
Generator[GEN_B]= TGenerator.Create(TGenerator.SQUARE)
Generator[GEN_C]= TGenerator.Create(TGenerator.SQUARE)
Generator[GEN_N]= TGenerator.Create(TGenerator.NOISE)
End Function


Function Stop()
Miniaudio.CloseDevice()
End Function


Function AskPeriod(Hertz:Int, High:Int Var, Low:Int Var)
'informs about how to fill the both register to hear a certain frequency
Hertz = PROCESS_CLOCK /16 / Hertz
High  = Hertz /256
Low   = Hertz Mod 256
End Function


' developer functions ***********************************************
Function SetRegister(Adress:Int, Value:Int)

Select Adress
Case 0, 2, 4
Value= Value & %11111111
Case 1, 3, 5, 13
Value= Value & %1111
Case 6, 8, 9, 10
Value= Value & %11111
Case 7, 11, 12, 14, 15
End Select

Register[Adress]=Value

Select Adress
Case 0,1,2,3,4,5
' frequency control
Adress = Adress/2
SetPeriod Adress+1, Register[2*Adress+1]*256+Register[2*Adress]
Case 6
' noise frequency control
SetPeriod GEN_N, Register[6] *16   ' sound better then *2
Case 7
' mixer on/off control
Print "register 7 ======>"+ Register[7]
Case 8,9,10
' volume control
SetVolume Adress-7, Register[Adress]  & %1111

End Select
End Function


' private functions ***********************************************
Function MyCallBack(void:Byte Ptr, PlayBuffer:Short Ptr, RecordingBuffer:Short Ptr, Frames:Int)
For Local i:Int=0 Until Frames
PlayBuffer[i] = Mixer()
Next
End Function


Function Mixer:Int()
Local Sample:Int, NoiseSample:Int = Generator[GEN_N].OneStep()
For Local i:Int=1 To 3
' this is experimental volume change WITH waiting for ZERO-cross:
' .... * Generator[i].Volume
Sample = Sample + (Generator[i].OneStep() * ChannelOn(i) + NoiseSample * ChannelOn(i+3)) * Generator[i].Volume

' this would be volume change WITHOUT waiting for ZERO-cross:
' feature or bug?
' .... * OutVolume[i]
Next
Return Sample
End Function


Function ChannelOn:Int(Bit:Int)
Bit = 1 Shl (bit-1)
Return Not (Register[7] & Bit)
End Function


Function SetPeriod(Nr:Int,Period:Int)
If Period=0
Generator[Nr].Period=0
Else
Generator[Nr].Period  = (Double(SAMPLE_RATE*Period*8)/PROCESS_CLOCK)
EndIf
End Function


Function SetVolume(Nr:Int,Volume:Int)
OutVolume[Nr] = VolTable[Volume]
Generator[Nr].NextVolume  = VolTable[Volume]
End Function


Function BuildVolumeTable(faktor:Double, Multiplicator:Int)
For Local i:Int= 1 To 15
VolTable[i] = faktor^(15-i)*Multiplicator
Next
End Function
End Type
 

Type TGenerator
    Const SQUARE:Int=1, NOISE:Int=2

Field Form:Int, Period:Double, Counter:Double, State:Int, Volume:Int, NextVolume:Int

Function Create:TGenerator(Form:Int)
Local loc:TGenerator = New TGenerator
loc.Form  = Form
loc.State = 1
Return loc
End Function

Method OneStep:Int()
'returns the sample value of this generator
If Counter<0.5
If Form=SQUARE
Counter:+ Period
Else ' NOISE
Counter = Rand(2,Period)
EndIf
State   = - State
Volume  = NextVolume
Return 0
EndIf
Counter:-1
Return State
End Method
End Type
'*******************************************************************
'  End OF THE YM2149 CLASS
'*******************************************************************



'*******************************************************************
' INTERACTION WITH GUI:
'*******************************************************************


Function CheckGUI()
Local Who:Int = TGui.CheckAll()
If Who>0
If Gadget[Who].Changed
Gadget[Who].Changed=False
SendAChange (Who)
EndIf
EndIf
End Function


Function SendAChange(Who:Int)
' handles the gadgets and reacts with sending to registers
Select Who
Case 1,5,9
' TONE period
Local high:Int=Int(Who/4)*2
Local period:Int=2900*Gadget[Who].Result
YM.SetRegister high+1, period/256
YM.SetRegister high  , period Mod 256

Case 2,6,10
' CHANNEL volume
' 8,9,10
Local volume:Int=20*Gadget[Who].Result -2
volume=Max(0,Min(15,volume))
If Who=2 Then Who=8
If Who=6 Then Who=9
YM.SetRegister Who, volume

Case 3,4,7,8,11,12
Local mute:Int = Gadget[3].state + 2*Gadget[7].state + 4*Gadget[11].state + 8*Gadget[4].state + 16*Gadget[8].state+ 32*Gadget[12].state
Print Bin(mute)
YM.SetRegister 7, mute
' MUTE CONTROL (mixer)
Case 13
' NOISE period
Local period:Int=31*Gadget[Who].Result
Print "noise " + period + " " + Gadget[Who].Result
YM.SetRegister 6, period
End Select
End Function


Function DefineDisplay()
' creates the GUI

' GADGETS-NUMBERS:
'A-Generator       1=period    2=volume     3=tone ON     4=Noise on
'B-Generator       5=period    6=volume     7=tone ON     8=Noise on
'C-Generator       9=period   10=volume    11=tone ON    12=Noise on
'Noise-Generator  13=period

For Local i:Int=0 To 2
Gadget[i*4+1] = New TGui (i*4+1, Chr(65+i) + "-TONE" ,  30, i*100+100, 400,40)
Gadget[i*4+2] = New TGui (i*4+2, "VOLUME"            , 450, i*100+100, 100,40)
Gadget[i*4+3] = New TGui (i*4+3, "T-ON"              , 580, i*100+100,  40,40)
Gadget[i*4+4] = New TGui (i*4+4, "N-ON"              , 650, i*100+100,  40,40)
Next
Gadget[13]        = New TGui (   13, "NOISE"             , 720, 100      , 40,240)
End Function



'*******************************************************************
' A VERY SIMPLE GUI:
'*******************************************************************

Type TGui
Global mX:Int, mY:Int, DontCheck:Int
Global Gadgets:TList = New TList

Field X:Int, y:Int, w:Int, h:Int, Name:String, ID:Int, Changed:Int
Field Active:Int, State:Int=0, Result:Double

Method New( _id:Int, Text:String, xX:Int, yy:Int, ww:Int, hh:Int)
    ID   = _id
Name = Text
x    = xx
y    = yy
w    = ww
h    = hh
If w=h
State= 1
Else
Result=0.8
EndIf 
Gadgets.addlast Self
End Method

Function DrawAll()
For Local Gadget:TGui = EachIn Gadgets
Gadget.Draw
Next
End Function 

Method Draw()
If Active=True
SetColor 255,255,0
Else
SetColor 255,255,255
EndIf
DrawRect x,y,w,h
DrawText Name, x,y-15
If w<>h
SetColor 1,1,1
ElseIf State=True
SetColor 1,1,1
Else
SetColor 111,111,55
EndIf
DrawRect x+2,y+2,w-4,h-4

If Result > 0
SetColor 166,177,188
If w>h
DrawRect x+w*result-5,y,10,40
Else
DrawRect x, y+h*result-5,40,10
EndIf
EndIf
End Method


Function CheckAll:Int()
If MouseDown(1)=0
DeActivateAll
DontCheck=False
Return 0
EndIf
If DontCheck=True Return 0

Mx = MouseX()
My = MouseY()
For Local Gadget:TGui = EachIn Gadgets
If Gadget.check() Return Gadget.ID
Next
DontCheck=True
End Function 


Method Check:Int()
If mX<X Then Return False
If mY<Y Then Return  False
If mX>X+W Then Return False
If My>Y+H Then Return False
Local old:Double=Result
If w=h And Active=False
State   = Not State
Changed = True
ElseIf w>h
Result = mX-x
Result = result/w
ElseIf  h>w
Result = my-y
Result = result/h
EndIf
If Old<>Result
Changed=True
EndIf   
Active=True
Return True
End Method


Function DeActivateAll()
For Local Gadget:TGui = EachIn Gadgets
Gadget.Active=False
Next
End Function
End Type





 
...back from Egypt

Midimaster

Fill The Emulator With Atari Demos

If you tried the last code, you will find that the results of that player are boaring. So now I present how they used this chip in the 80th.

Speed is the solution. They changed the generators parameter extremly often. So they reaches this incredible and typical 80th computer game sound.



Now you can test in with the BlitzMax code from today

I added a player, that reads demofiles and sends it to the new emulator.

The format of the demo-files is descriped here:
http://leonard.oxg.free.fr/ymformat.html

This is all you need:
(You need to download the file tom.bin from the attachment)
SuperStrict

TSongPlayer.Load("tom.bin")

Global Time:Int, Speed:Int=20
Repeat
    ...
    If Time<MilliSecs()
        TSongPlayer.Play
        Time = MilliSecs() + Speed
    EndIf
    ...
Until AppTerminate()

Type TSongPlayer
    Global Data:Byte[]
    Global P:Byte Ptr
    Global Frames:Int, DataStart:Int, LastAdress:Int, Counter:Int
   
    Function Load(File:String)
        Data    = LoadByteArray(File)
        P = Data  'pointer to the song-data
       
        ' collecting header information
        FileID        = String.FromBytes( P  , 4)
        CheckID      = String.FromBytes( P+4, 8)
        Frames        = ReadAsInt(12)
        SongAttribut  = ReadAsInt(16)
        DigiDrums    = ReadAsShort(20)
        Clock        = ReadAsInt(22)
        Speed        = ReadAsShort(26)
        Jump          = ReadAsInt(28)
        Future        = ReadAsShort(32)
        'ReadDigiDrums
        SongName      = ReadAsNullString(LastAdress)
        Author        = ReadAsNullString(LastAdress)
        Comment      = ReadAsNullString(LastAdress)
        DataStart    = LastAdress
        EndCheck        = String.FromBytes( P+ Data.Length-4  , 4)
        ActivRegisters
    End Function   
   
    Function Play()
        ' main play function:
        For Local i:Int=0 To 15
                Local Adress:Int = DataStart + i* Frames + Counter
                    YM.SetRegister i, P[Adress]
                EndIf
            EndIf
        Next
        Counter = (Counter +1 ) Mod Frames 
    End Function


In the end the player grows to 200 lines. Here is the complete source code
You need to download the file tom.bin from the attachment

SuperStrict
Import mima.miniaudio
AppTitle = "YM-2149 Emulator & SongPlayer  www.midimaster.de"
Graphics 800,500
SetClsColor 1,77,55

YM.Start
YM.SetRegister 7, %111111

TSongPlayer.Load("tom.bin")
TSongPlayer.PrintInfo

Global Time:Int, Speed:Int=20
Repeat
    Cls
    If Time<MilliSecs()
        TSongPlayer.Play
        Time= MilliSecs() + Speed
    EndIf
    TSongPlayer.DrawRam
    TSongPlayer.DrawInfo
    Flip
Until AppTerminate()
YM.Stop
End



'*******************************************************************
' THE YM149 CHIP:
'*******************************************************************

Type YM
    Const PROCESS_CLOCK:Int= 1789770   
    Const SAMPLE_RATE:Int = 48000
    Const MONO:Int=1, ON:Int=1, OFF:Int=0
    ' Generator Names:
    Const GEN_A:Int=1, GEN_B:Int=2, GEN_C:Int=3, GEN_N:Int=4

    Global MiniAudio:TMiniAudio=New TMiniAudio
    Global Generator:TGenerator[5]
    Global Register:Int[16]

    Global VolTable:Int[16]
    Global OutVolume:Double[4]


' public functions ***********************************************   
    Function Start()
        MiniAudio.OpenDevice( MiniAudio.PLAYBACK, Miniaudio.FORMAT_S16, MONO, SAMPLE_RATE, MyCallBack)   
        MiniAudio.StartDevice()
        BuildVolumeTable 0.7 , 10000
        Generator[GEN_A]= TGenerator.Create(TGenerator.SQUARE)
        Generator[GEN_B]= TGenerator.Create(TGenerator.SQUARE)
        Generator[GEN_C]= TGenerator.Create(TGenerator.SQUARE)
        Generator[GEN_N]= TGenerator.Create(TGenerator.NOISE)
    End Function

   
    Function Stop()
        Miniaudio.CloseDevice()
    End Function

   
    Function AskPeriod(Hertz:Int, High:Int Var, Low:Int Var)
        'informs about how to fill the both register to hear a certain frequency
        Hertz = PROCESS_CLOCK /16 / Hertz
        High  = Hertz /256
        Low  = Hertz Mod 256
    End Function


' developer functions ***********************************************
    Function SetRegister(Adress:Int, Value:Int)

        Select Adress
            Case 0, 2, 4
                Value= Value & %11111111
            Case 1, 3, 5, 13
                Value= Value & %1111       
            Case 6, 8, 9, 10
                Value= Value & %11111                   
            Case 7, 11, 12, 14, 15
        End Select
       
        Register[Adress]=Value       

        Select Adress
            Case 0,1,2,3,4,5
                ' frequency control
                Adress = Adress/2
                SetPeriod Adress+1, Register[2*Adress+1]*256+Register[2*Adress]
            Case 6
                ' noise frequency control
                SetPeriod GEN_N, Register[6] *16  ' sound better then *2
            Case 7
                ' mixer on/off control
            Case 8,9,10
                ' volume control
                SetVolume Adress-7, Register[Adress]  & %1111
               
        End Select
    End Function


' private functions ***********************************************
    Function MyCallBack(void:Byte Ptr, PlayBuffer:Short Ptr, RecordingBuffer:Short Ptr, Frames:Int)
        For Local i:Int=0 Until Frames
            PlayBuffer[i] = Mixer()
        Next
    End Function


    Function Mixer:Int()
        Local Sample:Int, NoiseSample:Int = Generator[GEN_N].OneStep()
        For Local i:Int=1 To 3               
                ' this is experimental volume change WITH waiting for ZERO-cross:
                ' .... * Generator[i].Volume
                Sample = Sample + (Generator[i].OneStep() * ChannelOn(i) + NoiseSample * ChannelOn(i+3)) * Generator[i].Volume

                ' this would be volume change WITHOUT waiting for ZERO-cross:
                ' feature or bug?
                ' .... * OutVolume[i]
        Next
        Return Sample
    End Function
   
   
    Function ChannelOn:Int(Bit:Int)
        Bit = 1 Shl (bit-1)
        Return Not (Register[7] & Bit)
    End Function

   
    Function SetPeriod(Nr:Int,Period:Int)
        If Period=0
            Generator[Nr].Period=0
        Else
            Generator[Nr].Period  = (Double(SAMPLE_RATE*Period*8)/PROCESS_CLOCK)
        EndIf
    End Function
   

    Function SetVolume(Nr:Int,Volume:Int)
        OutVolume[Nr] = VolTable[Volume]
        Generator[Nr].NextVolume  = VolTable[Volume]
    End Function


    Function BuildVolumeTable(faktor:Double, Multiplicator:Int)
        For Local i:Int= 1 To 15
            VolTable[i] = faktor^(15-i)*Multiplicator
        Next   
    End Function
End Type
 

Type TGenerator
    Const SQUARE:Int=1, NOISE:Int=2

    Field Form:Int, Period:Double, Counter:Double, State:Int, Volume:Int, NextVolume:Int
   
    Function Create:TGenerator(Form:Int)
        Local loc:TGenerator = New TGenerator
        loc.Form  = Form
        loc.State = 1
        Return loc
    End Function

    Method OneStep:Int()
        'returns the sample value of this generator
        If Counter<0.5
            If Form=SQUARE
                Counter:+ Period
            Else ' NOISE
                Counter = Rand(2,Period)           
            EndIf
            State  = - State
            Volume  = NextVolume
            Return 0
        EndIf
        Counter:-1
        Return State
    End Method
End Type



'*******************************************************************
'  THE YM-MUSIC PLAYER
'*******************************************************************



Type TSongPlayer
    Global Data:Byte[], LastRegister:Int[16], RegisterActiv:Int[16]
    Global P:Byte Ptr
    Global FileID:String, CheckID:String, SongName:String, Author:String, Comment:String, EndCheck:String
    Global Frames:Int, Clock:Int, Speed:Int, Jump:Int, DataStart:Int, LastAdress:Int, Counter:Int
   
    ' temp. undefined:
    Global  SongAttribut:Int, DigiDrums:Int, Future:Int
    'only for displaying:
    Global RamText:String [20]

    Function Load(File:String)
        Data    = LoadByteArray(File)
        P = Data
        FileID        = String.FromBytes( P  , 4)
        CheckID      = String.FromBytes( P+4, 8)
        Frames        = ReadAsInt(12)
        SongAttribut  = ReadAsInt(16)
        DigiDrums    = ReadAsShort(20)
        Clock        = ReadAsInt(22)
        Speed        = ReadAsShort(26)
        Jump          = ReadAsInt(28)
        Future        = ReadAsShort(32)
        ReadDigiDrums
        SongName      = ReadAsNullString(LastAdress)
        Author        = ReadAsNullString(LastAdress)
        Comment      = ReadAsNullString(LastAdress)
        DataStart    = LastAdress
        EndCheck        = String.FromBytes( P+ Data.Length-4  , 4)
        ActivRegisters
    End Function


    Function ReadDigiDrums()
        ' not implemented at the moment
        If DigiDrums>0
            Print "Last Adress before DigiDrums: " + LastAdress
            For Local i:Int=0 Until DigiDrums
                LastAdress:+ ReadAsInt(LastAdress)
            Next
            Print "Last Adress after DigiDrums: " + LastAdress
        EndIf   
    End Function   
       
   
    Function Play()
        Print counter
        '  do this only for displaying:
            CreateRamStrings(Counter)
           
        ' main play function:
        For Local i:Int=0 To 15
            If RegisterActiv[i]=True
                Local Adress:Int = DataStart + i* Frames + Counter
                ' send only changes:
                If LastRegister[i] <> P[Adress]
                    LastRegister[i] = P[Adress]
                    YM.SetRegister i, LastRegister[i]
                    Print "different  REG=" + i + "    V=" + P[Adress]
                EndIf
            EndIf
        Next
        counter = (Counter +1 ) Mod Frames 
    End Function


    Function ActivRegisters()
        'find registers, which are really used in the file
        For Local reg:Int=0 To 15
            For Local pos:Int=0 Until Frames
                Local Adress:Int = DataStart + reg* Frames + pos
                If P[Adress]>0
                    RegisterActiv[reg]=True
                    Print "Register " + reg + " ist aktiv"
                    Exit
                EndIf
            Next
        Next   
    End Function

   
    ' some help functions for byte reading:
   
    Function ReadAsInt:Int(Adress:Int)
        LastAdress = Adress+4
        Return P[Adress+0] Shl 24 + P[Adress+1] Shl 16 + P[Adress+2] Shl 8 + P[Adress+3]
    End Function
   
    Function ReadAsShort:Int(Adress:Int)
        LastAdress = Adress+2
        Return P[Adress+0] Shl 8 + P[Adress+1]
    End Function

    Function ReadAsNullString:String(Adress:Int)
        LastAdress = Adress
        Local locTexT:String
        Repeat
            locText:+ Chr(P[LastAdress])
            LastAdress:+1
        Until P[LastAdress]=0
        LastAdress:+1
        Return locText
    End Function

    Function Hex2:String(Value:Int)
        Return Hex(Value,2)
    End Function

    Function Hex:String(Value:Int, Digit:Int)
        Return Right(Hex(value),Digit)
    End Function




    Function DrawRam()
        ' only for displaying:   
        Local Yoff_Reg:Int=150
   
        SetColor 55,55,55
        DrawRect (counter Mod 16)*40+105,Yoff_Reg,33,340
        SetColor 255,255,255
        DrawText RamText[19],10,Yoff_Reg+20
        DrawText "-------------------------------------------------------------------------------------------------",10,Yoff_Reg+30
        For Local i:Int=0 To 15
            If RegisterActiv[i]=True
                DrawText RamText[i] ,10,Yoff_Reg+i*20+40
            EndIf
        Next
        SetColor 255,255,0
        DrawText counter ,70,Yoff_Reg+20
        For Local i:Int=0 To 15
            If RegisterActiv[i]=True
                DrawText hex2(LastRegister[i]) ,70,Yoff_Reg+i*20+40
            EndIf
        Next
        DrawText "-------------------------------------------------------------------------------------------------",10,Yoff_Reg+15*20+40
        DrawText "-------------------------------------------------------------------------------------------------",10,Yoff_Reg+15*20+40
    End Function

    Function DrawInfo()
        ' only for displaying:
        SetColor 255,111,0
        Local YoffInfo:Int=10
        DrawText "              H E A D E R - I N F O",10 ,YoffInfo
        DrawText "  Song Name : " + SongName + "              Author : " + Author,10 ,YoffInfo+20
        DrawText "    Comment : " + Comment,10 ,YoffInfo+40
        DrawText "    File-ID : " + FileID  + "            Check-ID : " + CheckID +"          End Check : " + EndCheck ,10 ,YoffInfo+60
        DrawText "      Frames : " + Frames +"        Data Adress : " + DataStart +"          Loop Jump : " + Jump + "        SongAttribut : " + SongAttribut,10 ,YoffInfo+80
        DrawText "      Clock : " + Clock + "Hz" + "          Song Speed : " + Speed + "Hz",10 ,YoffInfo+100
    End Function

    Function CreateRamStrings(Adress:Int)
    ' only for displaying:
        If counter Mod 16 > 0 Return
               
        Local n:String = "ADRESS          "
            For Local i:Int =0 To 15
                n=n + Right("  "+(Adress+i),3) + " |"
            Next
        RamText[19]=n
       
        For Local j:Int =0 To 15
            If RegisterActiv[j]=True

                Local t:String = "REG " + Right(" " + j,2)    +"          "   
                For Local i:Int =0 To 15
                    Local value:Int = P[DataStart + i + counter + j*Frames]
                    If value=P[DataStart + i + counter + j*Frames-1]
                        t=t + "    "
                   
                    Else
                        t=t + " " + Hex2(value) + "  "
                    EndIf
                Next
                RamText[j]=t
            EndIf
        Next
    End Function
   
   
       
    Function Print32Bytes(Adress:Int)
        ' only for debugging:
        Local t:String = "BYTES ", n:String = "ADRES ", a:String = "ASCII "
        For Local i:Int =0 To 20
            Local value:Int = P[Adress+i]
            If value<32 Or value>127
                a = a + "  - |"
            Else
                a = a + "  " + Chr(value) + " |"           
            EndIf
            t=t + " " + Hex2(value) + " |"
            n=n + Right("  "+(Adress+i),3) + " |"       
        Next
        Print n
        Print t
        Print a
    End Function

   
    Function PrintInfo()
        ' only for debugging:
        Print
        Print "****  I N F O  H E A D E R  *********"
        Print
        Print "    File-ID : " + FileID  + "<-->YM6!"
        Print "    Check-ID : " + CheckID + "<-->LeOnArD!"
        Print "      Frames : " + Frames
        Print "SongAttribut : " + SongAttribut
        Print "  DigiDrums : " + DigiDrums
        Print "      Clock : " + Clock + "Hz"
        Print "  Song Speed : " + Speed + "Hz"
        Print "  Loop Jump : " + Jump
        Print "  Song Name : " + SongName
        Print "      Author : " + Author
        Print "    Comment : " + Comment
        Print " Data Adress : " + DataStart
        Print "  End Check : " + EndCheck + "<-->End!"
        Print
        Print "****    E N D      *********"
        Print
    End Function
End Type




'*******************************************************************
'  End OF THE YM2149 CLASS
'*******************************************************************
...back from Egypt

Midimaster

Minimalistic AY-Player

We divide the code into two parts: Your main app and the YM2149_CLASS.bmx. So this would now be enough to run a Retro-Song:

SuperStrict
Import "YM2149_CLASS.bmx"
Graphics 400,300

YM.Start
YM.LoadSong("tom.bin")

Global Time:Int, Speed:Int=20
Repeat
    Cls
    If Time<MilliSecs()
        YM.PlaySong
        Time= MilliSecs() + Speed
    EndIf
   
    If KeyHit(KEY_S) Then YM.StopSong
    If KeyHit(KEY_P) Then YM.StartSong

    Flip 0
Until AppTerminate()
YM.Stop
End

you will find the "tom.bin" in the attachment.


Therefore we need to add some code lines to the CLASS and protect wide parts of the code from beeing accessed from outside:


Player related functions in the YM2149_CLASS.bmx:
    Function LoadSong(File:String)
        TYM_Player.Load(file)
        TYM_Player.PrintInfo
    End Function
   
   
    Function PlaySong()
        TYM_Player.Play
    End Function

   
    Function StopSong()
        TYM_Player.Enabled=False
        SetRegister 7, %111111
    End Function


    Function StartSong()
        TYM_Player.Counter=0
        TYM_Player.Enabled=True
    End Function


We closed all registeres from OutSide access like it is in the original chip. If you now want to access the registers from outside you need to to it in two steps via the same function SetDataPins():

Input function in the YM2149_CLASS.bmx:
    Function SetDataPins:Int(BDir:Int, BC1:Int, Value:Int )   
        ' main communication function
        '   TRUE,  TRUE, xxx = set adress
        '   TRUE, FALSE, xxx = set value
        '
        If BDir=False And BC1=False Then Return 0
            ' inactive
        If BDir=True And BC1=True
            ' set Register adress
            LatchedRegister = Value & %1111
        Else If BDir=True And BC1=False
            ' fill latched Register
            SetRegister( LatchedRegister, Value)
        Else   'If BDir=False And BC1=True
            ' read latched Register
            Return Register[LatchedRegister]
        EndIf
        Return 0
    End Function

There are only 10 PINs at the Chip, which we can access. I simulate this with the parameters BDir (1st Pin) and BC1 (2nd Pin) and "VALUE" (which means the eight PINS A0 to A7)

The access happens in two steps:
1.
With the first step you tell the chip, which register you want to fill. Value is from 0 to 15 for Registers 0 to 15. BDir and BC1 need to be TRUE
2.
In the second step you send the content, you want to transfer to this register. Here Value is from 0 to 255. BDir need to be TRUE. And BC1 need to be FALSE



The YM2149_CLASS.bmx


SuperStrict
Import mima.miniaudio
'*******************************************************************
'    YM2149-CLASS  Sound Chip Emulation  Version 1.0
'
'    Copyright: Peter Wolkersdorfer info@midimaster.de
'    PD: Public Domain (everybody can use the code as you like)
'
'    Source codes: https://github.com/MidimasterSoft
'    (mima.miniaudio.mod, ym2149_class.bmx, miniaudio.h)
'
'    written with BlitzMax NG:   https://www.blitzmax.org
'    Copyright MiniAudio-class:  https://github.com/mackron/miniaudio
'
'*******************************************************************
' THE YM2149 CHIP:
'*******************************************************************

Type YM
    Private
        Const PROCESS_CLOCK:Int= 1789770   
        Const SAMPLE_RATE:Int = 48000
        Const MONO:Int=1, ON:Int=1, OFF:Int=0
        ' Generator Names:
        Const GEN_A:Int=1, GEN_B:Int=2, GEN_C:Int=3, GEN_N:Int=4

        Global MiniAudio:TMiniAudio=New TMiniAudio
        Global Generator:TGenerator[5]
        Global Register:Int[16], LatchedRegister:Int

        Global VolTable:Int[16]
        Global OutVolume:Double[4]
   
'   public functions ***********************************************   
    Public

    Function Start()
        MiniAudio.OpenDevice( MiniAudio.PLAYBACK, Miniaudio.FORMAT_S16, MONO, SAMPLE_RATE, MyCallBack)   
        MiniAudio.StartDevice()
        BuildVolumeTable 0.7 , 10000
        Generator[GEN_A]= TGenerator.Create(TGenerator.SQUARE)
        Generator[GEN_B]= TGenerator.Create(TGenerator.SQUARE)
        Generator[GEN_C]= TGenerator.Create(TGenerator.SQUARE)
        Generator[GEN_N]= TGenerator.Create(TGenerator.NOISE)
        SetRegister 7, %111111
    End Function

   
    Function Stop()
        TYM_Player.Enabled=False
        Miniaudio.CloseDevice()
    End Function

   
    Function AskPeriod(Hertz:Int, High:Int Var, Low:Int Var)
        'informs about how to fill the both register to hear a certain frequency
        Hertz = PROCESS_CLOCK /16 / Hertz
        High  = Hertz /256
        Low   = Hertz Mod 256
    End Function


    Function SetDataPins:Int(BDir:Int, BC1:Int, Value:Int )   
        ' main communication function
        '   TRUE,  TRUE, xxx = set adress
        '   TRUE, FALSE, xxx = set value
        '
        If BDir=False And BC1=False Then Return 0
            ' inactive
        If BDir=True And BC1=True
            ' set Register adress
            LatchedRegister = Value & %1111
        Else If BDir=True And BC1=False
            ' fill latched Register
            SetRegister( LatchedRegister, Value)
        Else   'If BDir=False And BC1=True
            ' read latched Register
            Return Register[LatchedRegister]
        EndIf
        Return 0
    End Function
   
   
    Function LoadSong(File:String)
        TYM_Player.Load(file)
        TYM_Player.PrintInfo
    End Function
   
   
    Function PlaySong()
        TYM_Player.Play
    End Function

   
    Function StopSong()
        TYM_Player.Enabled=False
        SetRegister 7, %111111
    End Function


    Function StartSong()
        TYM_Player.Counter=0
        TYM_Player.Enabled=True
    End Function
   
' protected functions ***********************************************
    Protected
   
    Function SetRegister(Adress:Int, Value:Int)

        Select Adress
            Case 0, 2, 4
                Value= Value & %11111111
            Case 1, 3, 5, 13
                Value= Value & %1111       
            Case 6, 8, 9, 10
                Value= Value & %11111                   
            Case 7, 11, 12, 14, 15
        End Select
       
        Register[Adress]=Value       

        Select Adress
            Case 0,1,2,3,4,5
                ' frequency control
                Adress = Adress/2
                SetPeriod Adress+1, Register[2*Adress+1]*256+Register[2*Adress]
            Case 6
                ' noise frequency control
                SetPeriod GEN_N, Register[6] *16   ' sound better then *2
            Case 7
                ' mixer on/off control
            Case 8,9,10
                ' volume control
                SetVolume Adress-7, Register[Adress]  & %1111
               
        End Select
    End Function


    Protected
    Function MyCallBack(void:Byte Ptr, PlayBuffer:Short Ptr, RecordingBuffer:Short Ptr, Frames:Int)
        For Local i:Int=0 Until Frames
            PlayBuffer[i] = Mixer()
        Next
    End Function


    Function Mixer:Int()
        Local Sample:Int, NoiseSample:Int = Generator[GEN_N].OneStep()
        For Local i:Int=1 To 3               
                ' this is experimental volume change WITH waiting for ZERO-cross:
                ' .... * Generator[i].Volume
                Sample = Sample + (Generator[i].OneStep() * ChannelOn(i) + NoiseSample * ChannelOn(i+3)) * Generator[i].Volume

                ' this would be volume change WITHOUT waiting for ZERO-cross:
                ' feature or bug?
                ' .... * OutVolume[i]
        Next
        Return Sample
    End Function
   
   
    Function ChannelOn:Int(Bit:Int)
        Bit = 1 Shl (bit-1)
        Return Not (Register[7] & Bit)
    End Function

   
    Function SetPeriod(Nr:Int,Period:Int)
        If Period=0
            Generator[Nr].Period=0
        Else
            Generator[Nr].Period  = (Double(SAMPLE_RATE*Period*8)/PROCESS_CLOCK)
        EndIf
    End Function
   

    Function SetVolume(Nr:Int,Volume:Int)
        OutVolume[Nr] = VolTable[Volume]
        Generator[Nr].NextVolume  = VolTable[Volume]
    End Function


    Function BuildVolumeTable(faktor:Double, Multiplicator:Int)
        For Local i:Int= 1 To 15
            VolTable[i] = faktor^(15-i)*Multiplicator
        Next    
    End Function
End Type
 

Type TGenerator
    ' 3 generators to be driven via YM_Class
    Protected

    Const SQUARE:Int=1, NOISE:Int=2

    Field Form:Int, Period:Double, Counter:Double, State:Int, Volume:Int, NextVolume:Int
   
    Function Create:TGenerator(Form:Int)
        Local loc:TGenerator = New TGenerator
        loc.Form  = Form
        loc.State = 1
        Return loc
    End Function

    Method OneStep:Int()
        'returns the sample value of this generator
        If Counter<0.5
            If Form=SQUARE
                Counter:+ Period
            Else ' NOISE
                Counter = Rand(2,Period)           
            EndIf
            State   = - State
            Volume  = NextVolume
            Return 0
        EndIf
        Counter:-1
        Return State
    End Method
End Type



'*******************************************************************
'  End OF THE YM2149 CLASS
'*******************************************************************


Type TYM_Player
    ' minimalistic YM.file player to be driven via YM_Class
    Protected

    Global Data:Byte[], LastRegister:Int[16], RegisterActiv:Int[16]
    Global P:Byte Ptr
    Global FileID:String, CheckID:String, SongName:String, Author:String, Comment:String, EndCheck:String
    Global Frames:Int, Clock:Int, Speed:Int, Jump:Int, DataStart:Int, LastAdress:Int, Counter:Int
    Global Enabled:Int
   
    ' temp. undefined:
    Global  SongAttribut:Int, DigiDrums:Int, Future:Int

    Function Load(File:String)
        Data     = LoadByteArray(File)
        P = Data
        FileID        = String.FromBytes( P  , 4)
        CheckID       = String.FromBytes( P+4, 8)
        Frames        = ReadAsInt(12)
        SongAttribut  = ReadAsInt(16)
        DigiDrums     = ReadAsShort(20)
        Clock         = ReadAsInt(22)
        Speed         = ReadAsShort(26)
        Jump          = ReadAsInt(28)
        Future        = ReadAsShort(32)
        ReadDigiDrums
        SongName      = ReadAsNullString(LastAdress)
        Author        = ReadAsNullString(LastAdress)
        Comment       = ReadAsNullString(LastAdress)
        DataStart     = LastAdress
        EndCheck        = String.FromBytes( P+ Data.Length-4  , 4)
        ActivRegisters
        Enabled=True
    End Function


    Function ReadDigiDrums()
        ' not implemented at the moment
        If DigiDrums>0
            For Local i:Int=0 Until DigiDrums
                LastAdress:+ ReadAsInt(LastAdress)
            Next
        EndIf    
    End Function    
       
   
    Function Play()
        If Enabled=False Return
        ' main play function:
        For Local i:Int=0 To 15
            If RegisterActiv[i]=True
                Local Adress:Int = DataStart + i* Frames + Counter
                ' send only changes:
                If LastRegister[i] <> P[Adress]
                    LastRegister[i] = P[Adress]
                   
                    ' internal approach: direkt To the registers:                   
                    YM.SetRegister i, LastRegister[i]
                   
                    ' real world pin-related approach needs two steps:
                    ' YM. SetDataPins True, True , I
                    ' YM. SetDataPins True, False, LastRegister[i]
                EndIf
            EndIf
        Next
        counter = (Counter +1 ) Mod Frames 
    End Function

   
    Function ActivRegisters()
        'find registers, which are really used in the file
        For Local reg:Int=0 To 15
            For Local pos:Int=0 Until Frames
                Local Adress:Int = DataStart + reg* Frames + pos
                If P[Adress]>0
                    RegisterActiv[reg]=True
                    Exit
                EndIf
            Next
        Next    
    End Function

   
    ' some help functions for byte reading:
   
    Function ReadAsInt:Int(Adress:Int)
        LastAdress = Adress+4
        Return P[Adress+0] Shl 24 + P[Adress+1] Shl 16 + P[Adress+2] Shl 8 + P[Adress+3]
    End Function
   
    Function ReadAsShort:Int(Adress:Int)
        LastAdress = Adress+2
        Return P[Adress+0] Shl 8 + P[Adress+1]
    End Function

    Function ReadAsNullString:String(Adress:Int)
        LastAdress = Adress
        Local locTexT:String
        Repeat
            locText:+ Chr(P[LastAdress])
            LastAdress:+1
        Until P[LastAdress]=0
        LastAdress:+1
        Return locText
    End Function

    Function Hex2:String(Value:Int)
        Return Hex(Value,2)
    End Function

    Function Hex:String(Value:Int, Digit:Int)
        Return Right(Hex(value),Digit)
    End Function
   
    Function PrintInfo()
        ' only for debugging:
        Print
        Print "****  I N F O   H E A D E R  *********"
        Print
        Print "     File-ID : " + FileID  + "<-->YM6!"
        Print "    Check-ID : " + CheckID + "<-->LeOnArD!"
        Print "      Frames : " + Frames
        Print "SongAttribut : " + SongAttribut
        Print "   DigiDrums : " + DigiDrums
        Print "   CPU-Clock : " + Clock + "Hz"
        Print "  Song Speed : " + Speed + "Hz"
        Print "   Loop Jump : " + Jump
        Print "   Song Name : " + SongName
        Print "      Author : " + Author
        Print "     Comment : " + Comment
        Print " Data Adress : " + DataStart
        Print "   End Check : " + EndCheck + "<-->End!"
        Print
        Print "****    E N D       *********"
        Print
    End Function
End Type



Direct Access of the YM2149-Chip via PINs



here you see a user made player and how it is possible to access the Chip directly:

Import "YM2149_CLASS.bmx"

YM.Start
MySongPlayer.Load("tom.bin")

Global Time:Int, Speed:Int=20
Repeat
    ...
    If Time<MilliSecs()
        MySongPlayer.Play
        Time= MilliSecs() + Speed
    EndIf
    ...
Until AppTerminate()


Type MySongPlayer
    .....

    Function Play()
    ' main play function:
        For Local i:Int=0 To 15
                Local Adress:Int = DataStart + i* Frames + Counter
                   
                *************************************  H E R E : **********
                ' real world pin-related approach
                YM.SetDataPins True, True , i
                YM.SetDataPins True, False, P[Adress]
                ***********************************************************
        Next
        counter = (Counter +1 ) Mod Frames 
    End Function


And here is the complete code of the indiviual player with PIN-access:


SuperStrict
Import "YM2149_CLASS.bmx"

'*******************************************************************
'    YM2149  AY-Files Individual Song-Player  Version 1.0
'
'    Copyright: Peter Wolkersdorfer info@midimaster.de
'    PD: Public Domain (everybody can use the code as you like)
'
'    Source codes: https://github.com/MidimasterSoft
'    (mima.miniaudio.mod, ym2149_class.bmx, miniaudio.h)

AppTitle = "YM-2149 Emulator & Song-Player  www.midimaster.de"
Graphics 800,500
SetClsColor 1,77,55

YM.Start
MySongPlayer.Load("tom.bin")
MySongPlayer.PrintInfo


Global Time:Int, Speed:Int=20
Repeat
    Cls
    If Time<MilliSecs()
        MySongPlayer.Play
        Time= MilliSecs() + Speed
    EndIf
    MySongPlayer.DrawRam
    MySongPlayer.DrawInfo
   
    Flip 0
Until AppTerminate()
YM.Stop
End






Type MySongPlayer
    Global Data:Byte[], LastRegister:Int[16], RegisterActiv:Int[16]
    Global P:Byte Ptr
    Global FileID:String, CheckID:String, SongName:String, Author:String, Comment:String, EndCheck:String
    Global Frames:Int, Clock:Int, Speed:Int, Jump:Int, DataStart:Int, LastAdress:Int, Counter:Int
   
    ' temp. undefined:
    Global  SongAttribut:Int, DigiDrums:Int, Future:Int
    'only for displaying:
    Global RamText:String [20]

    Function Load(File:String)
        Data     = LoadByteArray(File)
        P = Data
        FileID        = String.FromBytes( P  , 4)
        CheckID       = String.FromBytes( P+4, 8)
        Frames        = ReadAsInt(12)
        SongAttribut  = ReadAsInt(16)
        DigiDrums     = ReadAsShort(20)
        Clock         = ReadAsInt(22)
        Speed         = ReadAsShort(26)
        Jump          = ReadAsInt(28)
        Future        = ReadAsShort(32)
        ReadDigiDrums
        SongName      = ReadAsNullString(LastAdress)
        Author        = ReadAsNullString(LastAdress)
        Comment       = ReadAsNullString(LastAdress)
        DataStart     = LastAdress
        EndCheck        = String.FromBytes( P+ Data.Length-4  , 4)
        ActivRegisters
    End Function


    Function ReadDigiDrums()
        ' not implemented at the moment
        If DigiDrums>0
            Print "Last Adress before DigiDrums: " + LastAdress
            For Local i:Int=0 Until DigiDrums
                LastAdress:+ ReadAsInt(LastAdress)
            Next
            Print "Last Adress after DigiDrums: " + LastAdress
        EndIf    
    End Function    
       
   
    Function Play()
        Print counter
        '   do this only for displaying:
            CreateRamStrings(Counter)
           
        ' main play function:
        For Local i:Int=0 To 15
            If RegisterActiv[i]=True
                Local Adress:Int = DataStart + i* Frames + Counter
                ' send only changes:
                If LastRegister[i] <> P[Adress]
                    LastRegister[i] = P[Adress]

                    ' real world pin-related approach
                    YM. SetDataPins True, True , I
                    YM. SetDataPins True, False, LastRegister[i]
                   
                    Print "different  REG=" + i + "    V=" + P[Adress]
                EndIf
            EndIf
        Next
        counter = (Counter +1 ) Mod Frames 
    End Function


    Function ActivRegisters()
        'find registers, which are really used in the file
        For Local reg:Int=0 To 15
            For Local pos:Int=0 Until Frames
                Local Adress:Int = DataStart + reg* Frames + pos
                If P[Adress]>0
                    RegisterActiv[reg]=True
                    Print "Register " + reg + " ist aktiv"
                    Exit
                EndIf
            Next
        Next    
    End Function

   
    ' some help functions for byte reading:
   
    Function ReadAsInt:Int(Adress:Int)
        LastAdress = Adress+4
        Return P[Adress+0] Shl 24 + P[Adress+1] Shl 16 + P[Adress+2] Shl 8 + P[Adress+3]
    End Function
   
    Function ReadAsShort:Int(Adress:Int)
        LastAdress = Adress+2
        Return P[Adress+0] Shl 8 + P[Adress+1]
    End Function

    Function ReadAsNullString:String(Adress:Int)
        If P[Adress]=0 Then Return "(no tag)"
        LastAdress = Adress
        Local locTexT:String
        Repeat
            locText:+ Chr(P[LastAdress])
            LastAdress:+1
        Until P[LastAdress]=0
        LastAdress:+1
        Return locText
    End Function

    Function Hex2:String(Value:Int)
        Return Hex(Value,2)
    End Function

    Function Hex:String(Value:Int, Digit:Int)
        Return Right(Hex(value),Digit)
    End Function




    Function DrawRam()
        ' only for displaying:    
        Local Yoff_Reg:Int=150
   
        SetColor 55,55,55
        DrawRect (counter Mod 16)*40+105,Yoff_Reg,33,340
        SetColor 255,255,255
        DrawText RamText[19],10,Yoff_Reg+20
        DrawText "-------------------------------------------------------------------------------------------------",10,Yoff_Reg+30
        For Local i:Int=0 To 15
            If RegisterActiv[i]=True
                DrawText RamText[i] ,10,Yoff_Reg+i*20+40
            EndIf
        Next
        SetColor 255,255,0
        DrawText counter ,70,Yoff_Reg+20
        For Local i:Int=0 To 15
            If RegisterActiv[i]=True
                DrawText hex2(LastRegister[i]) ,70,Yoff_Reg+i*20+40
            EndIf
        Next
        DrawText "-------------------------------------------------------------------------------------------------",10,Yoff_Reg+15*20+40
        DrawText "-------------------------------------------------------------------------------------------------",10,Yoff_Reg+15*20+40
    End Function

    Function DrawInfo()
        ' only for displaying:
        SetColor 255,111,0
        Local YoffInfo:Int=10
        DrawText "               H E A D E R - I N F O",10 ,YoffInfo
        DrawText "   Song Name : " + SongName + "              Author : " + Author,10 ,YoffInfo+20
        DrawText "     Comment : " + Comment,10 ,YoffInfo+40
        DrawText "     File-ID : " + FileID  + "            Check-ID : " + CheckID +"           End Check : " + EndCheck ,10 ,YoffInfo+60
        DrawText "      Frames : " + Frames +"         Data Adress : " + DataStart +"          Loop Jump : " + Jump + "        SongAttribut : " + SongAttribut,10 ,YoffInfo+80
        DrawText "       Clock : " + Clock + "Hz" + "          Song Speed : " + Speed + "Hz",10 ,YoffInfo+100
    End Function

    Function CreateRamStrings(Adress:Int)
    ' only for displaying:
        If counter Mod 16 > 0 Return
               
        Local n:String = "ADRESS           "
            For Local i:Int =0 To 15
                n=n + Right("   "+(Adress+i),3) + " |"
            Next
        RamText[19]=n
       
        For Local j:Int =0 To 15
            If RegisterActiv[j]=True

                Local t:String = "REG " + Right(" " + j,2)    +"           "   
                For Local i:Int =0 To 15
                    Local value:Int = P[DataStart + i + counter + j*Frames]
                    If value=P[DataStart + i + counter + j*Frames-1]
                        t=t + "     "
                   
                    Else
                        t=t + " " + Hex2(value) + "  "
                    EndIf
                Next
                RamText[j]=t
            EndIf
        Next
    End Function
   
   
       
    Function Print32Bytes(Adress:Int)
        ' only for debugging:
        Local t:String = "BYTES ", n:String = "ADRES ", a:String = "ASCII "
        For Local i:Int =0 To 20
            Local value:Int = P[Adress+i]
            If value<32 Or value>127
                a = a + "  - |"
            Else
                a = a + "  " + Chr(value) + " |"           
            EndIf
            t=t + " " + Hex2(value) + " |"
            n=n + Right("   "+(Adress+i),3) + " |"       
        Next
        Print n
        Print t
        Print a
    End Function

   
    Function PrintInfo()
        ' only for debugging:
        Print
        Print "****  I N F O   H E A D E R  *********"
        Print
        Print "     File-ID : " + FileID  + "<-->YM6!"
        Print "    Check-ID : " + CheckID + "<-->LeOnArD!"
        Print "      Frames : " + Frames
        Print "SongAttribut : " + SongAttribut
        Print "   DigiDrums : " + DigiDrums
        Print "       Clock : " + Clock + "Hz"
        Print "  Song Speed : " + Speed + "Hz"
        Print "   Loop Jump : " + Jump
        Print "   Song Name : " + SongName
        Print "      Author : " + Author
        Print "     Comment : " + Comment
        Print " Data Adress : " + DataStart
        Print "   End Check : " + EndCheck + "<-->End!"
        Print
        Print "****    E N D       *********"
        Print
    End Function
End Type


...back from Egypt