Jump to content
IGNORED

TIA Help (Hardware and Software (Sound Emulation))


johnnye79

Recommended Posts

I'm playing around both with emulation code (porting Stella's code to python for testing) and with hardware (I have 2 TIA chips, one from eBay and one that I extracted from a 2600 Jr).

 

Basically I'd love to write a single channel open source VSTi for the TIA (note: I am not concerned with the $95 Plogue, and right now don't care about emulating the POKEY or any other chipset) that you can plug into your DAW (note: TIATracker does not count, that's a tracker not a VSTi).

 

(Note: this is also for hobby, fun, learning, and most importantly sharing :) )

 

I'd also like to toy around with the hardware on the TIA on one of my actual chips, and have it mostly wired up to my Arduino, but am running into issues and need help.

 

My Arduino mapping looks like this:

GND to 1 (Vss)

+5V to 20 (Vcc)

Crystal Oscillator output (3.5Mhz) to 11 (OSC)

D2-D7 on Arduino to TIA D0-D5 respectively, writing LSB to D0

A0-A5 on Arduino (writing as digitalWrite outputs) to TIA A0-A5 respectively, writing LSB to A0

D11 on Arduino to RW

D12 on Arduino to Phi-2

+5V to CS2, GND to CS1,CS3,CS4

AUD1/2 to LM386 amp to speakers

 

When I run it, my speakers make a horrible screaming sound like the sound on Doom 3 when you're going through a teleporter...??

 

Either my wiring needs to be fixed, my Arduino sketch (included below) needs work...

or I need a young priest, an old priest, some holy water, and some lasers for good measure.

 

Arduino code:

const unsigned char Sound[2] = {0x15,0x16}; // 4-bit D3-D0: 0 = voice 1, 1111 = voice 16
const unsigned char Freq[2] = {0x17,0x18}; // 5-bit D4-D0: 0 = no division, 11111 = divide by 32
const unsigned char Vol[2] = {0x19,0x1A}; // 4-bit D3-D0: 0 = no output, 1111 = highest

// Set RW to Low
// Set CS high then low to strobe

//const int Ready = 10;
const int ClockSync = 11; // Phi-2 on the Atari
const int ReadWrite = 12;

void setup() {
  Serial.begin(9600);
  delay(500);
  Serial.println("Initializing...");
  // put your setup code here, to run once:

  // data bus pins
  pinMode(2,OUTPUT);
  pinMode(3,OUTPUT);
  pinMode(4,OUTPUT);
  pinMode(5,OUTPUT);
  pinMode(6,OUTPUT);
  pinMode(7,OUTPUT);

  // address pins (set digital)
  pinMode(A0,OUTPUT);
  pinMode(A1,OUTPUT);
  pinMode(A2,OUTPUT);
  pinMode(A3,OUTPUT);
  pinMode(A4,OUTPUT);
  pinMode(A5,OUTPUT);

  //pinMode(Ready, INPUT);
  pinMode(ReadWrite, OUTPUT);
  // This is Phi-2 on the Atari
  pinMode(ClockSync, OUTPUT);

  initAtari();

}

void initAtari() {
  digitalWrite(ClockSync, 1); 
  digitalWrite(ReadWrite, 1); 
  delay(100);
}

void AtariWriteTone(int chan, unsigned char volume, unsigned char freq, unsigned char sound) {
  digitalWrite(ClockSync, 1); 
  digitalWrite(ReadWrite, 1); 
  delay(200);
  writeAddressByte(Sound[chan]);
  writeByteToDataBus(sound);
  digitalWrite(ClockSync, 0); 
  delay(100);
  digitalWrite(ReadWrite, 0); 
  delay(200);

  digitalWrite(ClockSync, 1); 
  digitalWrite(ReadWrite, 1); 
  delay(200);
  writeAddressByte(Freq[chan]);
  writeByteToDataBus(freq);
  digitalWrite(ClockSync, 0); 
  delay(100);
  digitalWrite(ReadWrite, 0); 
  delay(200);

  digitalWrite(ClockSync, 1); 
  digitalWrite(ReadWrite, 1); 
  delay(200);
  writeAddressByte(Vol[chan]);
  writeByteToDataBus(volume);
  digitalWrite(ClockSync, 0); 
  delay(100);
  digitalWrite(ReadWrite, 0); 
  delay(200);
}

void writeAddressByte(unsigned char data) {
  int b0 = data & 1;
  int b1 = (data >> 1) & 1;
  int b2 = (data >> 2) & 1;
  int b3 = (data >> 3) & 1;
  int b4 = (data >> 4) & 1;
  int b5 = (data >> 5) & 1;

  digitalWrite(A0,b0);
  digitalWrite(A1,b1);
  digitalWrite(A2,b2);
  digitalWrite(A3,b3);
  digitalWrite(A4,b4);
  digitalWrite(A5,b5);
}

void writeByteToDataBus(unsigned char data) {
  int b0 = data & 1;
  int b1 = (data >> 1) & 1;
  int b2 = (data >> 2) & 1;
  int b3 = (data >> 3) & 1;
  int b4 = (data >> 4) & 1;
  int b5 = (data >> 5) & 1;

  digitalWrite(2,b0);
  digitalWrite(3,b1);
  digitalWrite(4,b2);
  digitalWrite(5,b3);
  digitalWrite(6,b4);
  digitalWrite(7,b5);

}



void loop() {
  // put your main code here, to run repeatedly:
//  int rdy = digitalRead(Ready);
  //Serial.println("Ready: " + String(rdy));
  //delay(1000);

  for (int voice = 0; voice < 16; voice++ ) {
    for (int freq = 0; freq < 32; freq++) {
      Serial.println("Write to Channel 0...");
      AtariWriteTone(0,2,freq,voice);
      //Serial.println("Write to Channel 1...");
      //AtariWriteTone(1,2,freq,voice);
    }
  }

}

Now on to the software side. I've been working today on porting Stella's sound code over to Python for testing as kind of a starting point for my VSTi (this is how I learn, don't judge :P ). It looks like it works, but Instrument 11 (AUDC 0xB) just gives me a high pitched tone on all frequencies. Some of the other instruments I distinctly remember on the VCS being able to go higher frequency. I'm not sure where I'm screwing up here:

 

Python code (yeah I know, pyAudio is nasty, but it's a starting point):

import math
import pyaudio
import sys
import struct
from enum import Enum

class AUDCxRegister(Enum):
    SET_TO_1    = 0x00  #0000
    POLY4       = 0x01  #0001
    DIV31_POLY4 = 0x02  #0010
    POLY5_POLY4 = 0x03  #0011
    PURE1       = 0x04  #0100
    PURE2       = 0x05  #0101
    DIV31_PURE  = 0x06  #0110
    POLY5_2     = 0x07  #0111
    POLY9       = 0x08  #1000
    POLY5       = 0x09  #1001
    DIV31_POLY5 = 0x0a  #1010
    POLY5_POLY5 = 0x0b  #1011
    DIV3_PURE   = 0x0c  #1100
    DIV3_PURE2  = 0x0d  #1101
    DIV93_PURE  = 0x0e  #1110
    POLY5_DIV3  = 0x0f  #1111

class AUDFlags(Enum):
    POLY4_SIZE = 0x000f
    POLY5_SIZE = 0x001f
    POLY9_SIZE = 0x01ff
    DIV3_MASK  = 0x0c
    AUDV_SHIFT = 2
    #AUDV_SHIFT = 10     # shift 2 positions for AUDV,
                      # then another 8 for 16-bit sound

class TIA:
    # It made sense to initialize each TIA audio channel as a separate class object
    # decoupling it from the 2 audio channel coupling of the TIA on the Atari 2600, 
    # so that you could create multiple TIA channels for harmonies such as triad chords.

    # Structures to hold the 6 tia sound control bytes
    #myAUDC[2];    // AUDCx (15, 16)
    #myAUDF[2];    // AUDFx (17, 18)
    #myAUDV[2];    // AUDVx (19, 1A)

    myVolume = 0 # Last (final) output volume for each channel

    myP4 = 0     # Position pointer for the 4-bit POLY array
    myP5 = 0     # Position pointer for the 5-bit POLY array
    myP9 = 0     # Position pointer for the 9-bit POLY array

    myDivNCnt = 0 # Divide by n counter. one for each channel
    myDivNMax = 0 # Divide by n maximum, one for each channel
    myDiv3Cnt = 0 # Div 3 counter, used for POLY5_DIV3 mode

    #  ChannelMode myChannelMode;
    myOutputFrequency  = 0
    myOutputCounter    = 0
    myVolumePercentage = 0

    Bit4 = []
    Bit5 = []
    Bit9 = []
    Div31 = [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

    SoundControl = 0     # This takes the place of myAUDCx
    SoundFrequency = 440 # This takes the place of myAUDFx
    SoundVolume = 15     # This takes the place of myAUDVx
    SoundRate = 44100   

    ###################################
    def __init__(self):
        for x in range(0,AUDFlags.POLY4_SIZE):
            self.Bit4.append(0)
        for x in range(0,AUDFlags.POLY5_SIZE):
            self.Bit5.append(0)
        for x in range(0,AUDFlags.POLY9_SIZE):
            self.Bit9.append(0)
        self.reset()

    ###################################
    def reset(self):
        self.polyInit(self.Bit4, 4, 4, 3)
        self.polyInit(self.Bit5, 5, 5, 3)
        self.polyInit(self.Bit9, 9, 9, 5)

        self.myVolume = 0
        self.myDivNCnt = 0
        self.myDivNMax = 0
        self.myDiv3Cnt = 3
        self.SoundControl = 0
        self.SoundFrequency = 0
        self.SoundVolume = 0
        self.myP4 = 0
        self.myP5 = 0
        self.myP9 = 0

        self.myOutputCounter = 0

    ###################################
    def polyInit(self, poly, size, f0, f1):
        mask = (1 << size) - 1
        x = mask
        for i in range(0,mask):
            bit0 = ( (x >> (size - f0)) if ( size - f0 ) else x ) & 0x01
            bit1 = ( (x >> (size - f1)) if ( size - f1 ) else x ) & 0x01
            poly[i] = x & 1
            # calculate next bit
            x = ( x >> 1 ) | ( ( bit0 ^ bit1 ) << ( size - 1) )
  
    ###################################
    def setMasterVolume(self, percent):
        if (percent <= 100):
            self.myVolumePercentage = percent

    ###################################
    def setOutputFrequency(self, freq):
        self.myOutputFrequency = freq;

    ###################################
    def setChannel(self, address, value):
        if address == 0: # (usually this would be AUDC0/AUDC1)
            self.SoundControl = value & 0x0f
        if address == 1: # (usually this would be AUDF0/AUDF1)
            self.SoundFrequency = value # (remember that this number is usually divided by 31400Hz)
        if address == 2: # (usually this would be AUDV0/AUDV1) 
            self.SoundVolume = (value & 0x0f) << AUDFlags.AUDV_SHIFT
        newVal = 0

        # An AUDC value of 0 is a special case
        if self.SoundControl == AUDCxRegister.SET_TO_1 or self.SoundControl == AUDCxRegister.POLY5_POLY5:
            # Indicate the clock is zero so no processing will occur,
            # and set the output to the selected volume
            newVal = 0
            self.myVolume = (self.SoundVolume * self.myVolumePercentage) / 100.0
        else:
            # Otherwise calculate the 'divide by N' value
            newVal = self.SoundFrequency + 1
            
            # If bits 2 & 3 are set, then multiply the 'div by n' count by 3
            if ((self.SoundControl & AUDFlags.DIV3_MASK) == AUDFlags.DIV3_MASK and self.SoundControl != AUDCxRegister.POLY5_DIV3):
                newVal = newVal * 3

        # Only reset those channels that have changed
        if (newVal != self.myDivNMax):
            # Reset the divide by n counters
            self.myDivNMax = newVal
            
            # If the channel is now volume only or was volume only,
            # reset the counter (otherwise let it complete the previous)
            if ((self.myDivNCnt == 0) or (newVal == 0)):
                self.myDivNCnt = newVal
      
    ###################################
    def process(self, buffer, samples):
        bufferPos = 0
        # Make temporary local copy
        audc0 = self.SoundControl
        p5_0 = self.myP5
        div_n_cnt0 = self.myDivNCnt
        v0 = self.myVolume

        # Take external volume into account
        audv0 = (self.SoundVolume * self.myVolumePercentage) / 100

        # Loop until the sample buffer is full
        while(samples > 0):
            # Process channel 0
            if (div_n_cnt0 > 1):
                div_n_cnt0 -= 1
            elif (div_n_cnt0 == 1):
                prev_bit5 = self.Bit5[p5_0]
                div_n_cnt0 = self.myDivNMax

                # The P5 counter has multiple uses, so we increment it here
                p5_0 += 1
                if (p5_0 == AUDFlags.POLY5_SIZE):
                    p5_0 = 0

                # Check clock modifier for clock tick
                if ((audc0 & 0x02) == 0 or 
                    ((audc0 & 0x01) == 0 and self.Div31[p5_0] > 0) or 
                    ((audc0 & 0x01) == 1 and self.Bit5[p5_0] > 0) or 
                    ((audc0 & 0x0f) == AUDCxRegister.POLY5_DIV3 and self.Bit5[p5_0] != prev_bit5)):
                    if (audc0 & 0x04 > 0):       # Pure modified clock selected
                        if ((audc0 & 0x0f > 0) == AUDCxRegister.POLY5_DIV3): # POLY5 -> DIV3 mode
                            if ( Bit5[p5_0] != prev_bit5 ):
                                self.myDiv3Cnt -= 1
                                if ( myDiv3Cnt == 0 ):
                                    self.myDiv3Cnt = 3
                                    if (v0 > 0):
                                        v0 = 0
                                    else:
                                        v0 = audv0
                        else:
                            # If the output was set turn it off, else turn it on
                            if (v0 > 0):
                                v0 = 0
                            else:
                                v0 = audv0
                    elif (audc0 & 0x08 > 0):  # Check for p5/p9
                        if (audc0 == AUDCxRegister.POLY9):   # Check for poly9
                            # Increase the poly9 counter
                            self.myP9 += 1
                            if (self.myP9 == AUDFlags.POLY9_SIZE):
                                self.myP9 = 0
                            if (self.Bit9[self.myP9] > 0):
                                v0 = audv0
                            else:
                                v0 = 0
                        elif (audc0 & 0x02 > 0):
                            if (v0 > 0 or (audc0 & 0x01 > 0)):
                                v0 = 0
                            else:
                                v0 = audv0
                        else:  # Must be poly5
                            if (self.Bit5[p5_0] > 0):
                                v0 = audv0
                            else:
                                v0 = 0
                    else:  # Poly4 is the only remaining option
                        # Increase the poly4 counter
                        self.myP4 += 1
                        if (self.myP4 == AUDFlags.POLY4_SIZE):
                            self.myP4 = 0
                        if (self.Bit4[self.myP4] > 0):
                            v0 = audv0
                        else:
                            v0 = 0

            self.myOutputCounter += self.myOutputFrequency;

            while((samples > 0) and (self.myOutputCounter >= 31400)):
                buffer[bufferPos] = v0
                bufferPos += 1
                self.myOutputCounter -= 31400
                samples -= 1
                #print("TEST")

        # Save for next round
        self.myP5 = p5_0
        self.myVolume = v0
        self.myDivNCnt = div_n_cnt0


t = TIA()
t.setMasterVolume(100)
t.setOutputFrequency(44100)
t.setChannel(0,int(sys.argv[1]))
t.setChannel(2,int(sys.argv[2]))
bufferSize = int(sys.argv[3])
direction = int(sys.argv[4])
endBuffer = []
buffer = []
for x in range(0,bufferSize):
    buffer.append(0)
for x in range(0,32):
    freq = x if direction > 0 else 31-x
    t.setChannel(1,freq)
    t.process(buffer,bufferSize)
    endBuffer.extend(buffer)

print("Bit 4")
print(t.Bit4)
print("Bit 5")
print(t.Bit5)
print("Bit 9")
print(t.Bit9)
print("Div 31")
print(t.Div31)

p = pyaudio.PyAudio()

data = endBuffer
stream = p.open(format =
                p.get_format_from_width(1),
                channels = 1,
                rate = 44100,
                output = True)
stream.write(struct.pack('f'*len(data), *data))
stream.stop_stream()
stream.close()
p.terminate()

The next question of course is how to scale my waveforms so that I can use any piano frequency, but I'll save that one for after I get things up to a working state at least. My noob brain at Digital Signal Processing is a little fried right now on wrapping my mind around polynomial counters programmatic loops versus traditional waveforms that can expressed with Fourier series transforms and how to translate between the two. :P

Link to comment
Share on other sites

I don't think TIA ever worked with any variant of C or Phython. Have you tried ASM? ;)

 

Stella is written in C you know. :P

Would be great if Stella would let me put debug watches the AUDXX registers while playing without frame stepping in the debugger. Guess I'll have to mod the source at some point.

 

The software portion of my project is working at least, just had to set PyAudio to output 16-bit samples instead of 8-bit. (Even though my samples look like they're 8-bit?... O_o) Now to translate it all to a VSTi, there's of course still the issue of frequency translation. (Also turns out that I was smoking crack, instrument 11 is not played on the TIA apparently.)

Edited by johnnye79
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...