johnnye79 Posted September 20, 2016 Share Posted September 20, 2016 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 ). 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. Quote Link to comment Share on other sites More sharing options...
7800fan Posted September 20, 2016 Share Posted September 20, 2016 I don't think TIA ever worked with any variant of C or Phython. Have you tried ASM? Quote Link to comment Share on other sites More sharing options...
johnnye79 Posted September 20, 2016 Author Share Posted September 20, 2016 (edited) 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. 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 September 20, 2016 by johnnye79 Quote Link to comment Share on other sites More sharing options...
Recommended Posts
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.