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:

Code: BASIC
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.


...on the way to China.

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

Code: BASIC
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:

Code: BASIC
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:

Code: BASIC
    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:

Code: BASIC
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 



 
...on the way to China.

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:

Code: BASIC
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:

Code: BASIC
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:

Code: BASIC
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:
Code: BASIC
½-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:

Code: BASIC
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:

Code: BASIC
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 
...on the way to China.

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)

Code: BASIC
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
Code: BASIC
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
Code: BASIC
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:

Code: BASIC
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 



...on the way to China.

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:

Code: BASIC
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:

Code: BASIC
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:

Code: BASIC
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:

Code: BASIC
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:

Code: BASIC
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 



...on the way to China.

Midimaster

And Then They Were Three

The YM-2149 has three tone generators and one noise generator.
 
Code: BASIC
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

Code: BASIC
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:
Code: BASIC
              -- 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:
Code: BASIC
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:

Code: BASIC
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:

Code: BASIC
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:

Code: BASIC
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:

Code: BASIC
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:

Code: BASIC
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 





 
...on the way to China.

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)
Code: BASIC
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

Code: BASIC
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
'*******************************************************************
...on the way to China.

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:

Code: BASIC
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:
Code: BASIC
    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:
Code: BASIC
    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


Code: BASIC
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:

Code: BASIC
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:


Code: BASIC
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 


...on the way to China.