13 PCM_PERIODSIZE = 44100
15 # see https://www.tinaja.com/ebooks/tearing_rework.pdf
16 ZP_WNDLFT = 0x20 # Left side of scroll window
17 ZP_WNDWTH = 0x21 # Width of scroll window
18 ZP_WNDTOP = 0x22 # Top of scroll window
19 ZP_WNDBTM = 0x23 # Bottom of scroll window
20 ZP_CH = 0x24 # Cursor horizontal position
21 ZP_CV = 0x25 # Cursor vertical position
22 ZP_GBASL = 0x21 # LORES graphics base low
23 ZP_GBASH = 0x27 # LORES graphics base high
24 ZP_BASL = 0x28 # TEXT base address low
25 ZP_BASH = 0x29 # TEXT base address high
26 ZP_BAS2L = 0x2A # Scroll temporary base low
27 ZP_BAS2H = 0x2B # Scroll temporary base high
28 ZP_COLOR = 0x30 # Holds the LORES color value
29 ZP_INVFLG = 0x32 # Normal/Inverse/Flash mask
30 ZP_PROMPT = 0x33 # Holds prompt symbol
31 ZP_YSAV = 0x34 # Temporary Y register hold
32 ZP_CSWL = 0x36 # Output character hook low
33 ZP_CSWH = 0x37 # Output character hook high
34 ZP_KSWL = 0x38 # Input character hook low
35 ZP_KSWH = 0x39 # Input character hook high
36 ZP_ACC = 0x45 # Accumulator save
37 ZP_XREG = 0x46 # X register save
38 ZP_YREG = 0x47 # Y register save
39 ZP_STATUS = 0x48 # Flag register save
40 ZP_SPNT = 0x49 # Stack pointer save
41 ZP_RNDL = 0x4E # Keybounce random number low
42 ZP_RNDH = 0x4F # Keybounce random number high
43 VP_REENTER = 0x03D0 # Re-enter DOS
44 VP_RECONNECT = 0x03EA # Reconnect DOS I/O hooks
45 VP_BRKV = 0x03F0 # Break vector address
46 VP_SOFTEV = 0x03F2 # Warm start vector address
47 VP_PWRDUP = 0x03F4 # Warm start EOR A5 checksum
48 VP_AMPERV = 0x03F6 # Applesoft "&" Jump Code
49 VP_USRADR = 0x03F8 # Control Y vector Jump Code
50 VP_NMI = 0x03FB # NMI vector Jump Code
51 VP_IRQLOC = 0x03FE # Interrupt vector address
52 HW_IOADR = 0xC000 # Keyboard input location
53 HW_KBDSTRB = 0xC010 # Keyboard strobe reset
54 HW_TAPEOUT = 0xC020 # Cassette data output
55 HW_SPKR = 0xC030 # Speaker click output
56 HW_STROBE = 0xC040 # Game I/O connector strobe
57 HW_TXTCLR = 0xC050 # Graphics ON soft switch
58 HW_TXTSET = 0xC051 # Text ON soft switch
59 HW_MIXCLR = 0xC052 # Full screen ON soft switch
60 HW_MIXSET = 0xC053 # Split screen ON soft switch
61 HW_LOWSCR = 0xC054 # Page ONE display soft switch
62 HW_HISCR = 0xC055 # Page TWO display soft switch
63 HW_LORES = 0xC056 # LORES ON soft switch
64 HW_HIRES = 0xC057 # HIRES ON soft switch
65 HW_AN0CLR = 0xC058 # Annunciator 0 OFF soft switch
66 HW_AN0SET = 0xC059 # Annunciator 0 ON soft switch
67 HW_AN1CLR = 0xC05A # Annunciator 1 OFF soft switch
68 HW_AN1SET = 0xC05B # Annunciator 1 ON soft switch
69 HW_AN2CLR = 0xC05C # Annunciator 2 OFF soft switch
70 HW_AN2SET = 0xC05D # Annunciator 2 ON soft switch
71 HW_AN3CLR = 0xC05E # Annunciator 3 OFF soft switch
72 HW_AN3SET = 0xC05F # Annunciator 3 ON soft switch
73 HW_TAPEIN = 0xC060 # Cassette tape read input
74 HW_PB0 = 0xC061 # Push button 0 input
75 HW_PB1 = 0xC062 # Push button 1 input
76 HW_PB2 = 0xC063 # Push button 2 input
77 HW_PDL0 = 0xC064 # Game Paddle 0 analog input
78 HW_PDL1 = 0xC065 # Game Paddle 1 analog input
79 HW_PDL2 = 0xC066 # Game Paddle 2 analog input
80 HW_PDL3 = 0xC067 # Game Paddle 3 analog input
81 ROM_PLOT = 0xF800 # Plot a block on LORES screen
82 ROM_HLINE = 0xF819 # Draw a horizontal LORES line
83 ROM_VLINE = 0xF828 # Draw a vertical LORES line
84 ROM_CLRSCR = 0xF832 # Clear full LORES screen
85 ROM_CLRTOP = 0xF836 # Clear top of LORES screen
86 ROM_GBASCALC = 0xF847 # Calculate LORES base address
87 ROM_NEXTCOL = 0xF85F # Increase LORES color by three
88 ROM_SETCOL = 0xF864 # Set color for LORES plotting
89 ROM_SCRN = 0xF871 # Read color of LORES screen
90 ROM_PRNTAX = 0xF941 # Output A then X as hex
91 ROM_PRBLNK = 0xF948 # Output three spaces via hooks
92 ROM_PRBL2 = 0xF94A # Output X spaces via hooks
93 ROM_STEP = 0xFA43 # Single step (old ROM only!)
94 ROM_REGDSP = 0xFAD7 # Display working registers
95 ROM_PREAD = 0xFB1E # Read a game paddle
96 ROM_INIT = 0xFB2F # Initial i ze text screen
97 ROM_SETTXT = 0xFB39 # Set up text screen
98 ROM_SETGR = 0xFB40 # Setup LORES screen
99 ROM_SETWND = 0xFB4B # Set text window to normal
100 ROM_BASCALC = 0xFBC1 # Calculate text base address
101 ROM_BELL = 0xFBD9 # 1 Beep speaker if ctrl G
102 ROM_BELL2 = 0xFBE4 # Beep speaker once
103 ROM_ADVANCE = 0xFBF4 # Move text cursor right by one
104 ROM_VIDOUT = 0xFBFD # Output ASCII to screen only
105 ROM_BS = 0xFC10 # Backspace screen
106 ROM_UP = 0xFC1A # Move screen cursor up one
107 ROM_VTAB = 0xFC22 # Vertical screen tab using CV
108 ROM_VTABZ = 0xFC24 # Vertical screen tab using A
109 ROM_ESCl = 0xFC2C # Process escape movements A-G
110 ROM_CLREOP = 0xFC42 # Clear text to end of screen
111 ROM_HOME = 0xFC58 # Clear screen and home cursor
112 ROM_CR = 0xFC62 # Carriage return to screen
113 ROM_LF = 0xFC66 # Line feed to screen onIy
114 ROM_SCROLL = 0xFC70 # Scroll text screen up one
115 ROM_CLEOL = 0xFC9C # Clear text to end of line
116 ROM_WAIT = 0xFCA8 # Time delay set by accumulator
117 ROM_RDKEY = 0xFD0C # Get input character via hooks
118 ROM_KEYIN = 0xFD1B # Read the Apple keyboard
119 ROM_RDCHAR = 0xFD35 # Get key and process ESC A-F
120 ROM_CANCEL = 0xFD62 # Cancel keyboard line entry
121 ROM_GETLNZ = 0xFD67 # CR, then get kbrl input line
122 ROM_GETLN = 0xFD6A # Get input line from keyboard
123 ROM_GETLN1 = 0xFD6F # Get kbd input, no prompt
124 ROM_CROUT1 = 0xFD8B # Clear EOL then CR via hooks
125 ROM_CROUT = 0xFD8E # Output return via hooks
126 ROM_PRBYTE = 0xFDDA # Output full A in hpxto hooks
127 ROM_PRHEX = 0xFDE3 # Output low A in hex to hooks
128 ROM_COUT = 0xFDED # Output character via hooks
129 ROM_COUT1 = 0xFDF0 # Output character to screen
130 ROM_MOVE = 0xFE2C # Move block of memory
131 ROM_VERIFY = 0xFE36 # Verify block of memory
132 ROM_LIST = 0xFE5E # Disassemble 20 instructions
133 ROM_L1ST2 = 0xFE63 # Disassemble A instructions
134 ROM_SETINV = 0xFE80 # Print inverse text on screen
135 ROM_SETNORM = 0xFE84 # Print normal text on screen
136 ROM_SETVID = 0xFE93 # Grab output hooks for screen
137 ROM_XBASIC = 0xFEB0 # Goto BASIC, destroying old
138 ROM_BASCON = 0xFEB3 # Goto BASIC continuing old
139 ROM_TRACE = 0xFEC2 # Start tracing (old ROM only!)
140 ROM_WRITE = 0xFECD # Save to cassette tape
141 ROM_READ = 0xFEFD # Read from cassette tape
142 ROM_PRERR = 0xFF2D # Print "ERR" to output hook
143 ROM_BELL = 0xFF3A # Output bell via hooks
144 ROM_IORESR = 0xFF3F # Restore all working register
145 ROM_IOSAVE = 0xFF4A # Save all working registers
146 ROM_OLDRST = 0xFF59 # Old reset entry, no autostart
147 ROM_MON = 0xFF65 # Enter monitor and beep spkr
149 # see https://imgur.com/h0NNOc3
150 # colors (map 15 apple colors to 16 VT100 colours, some duplicate or unused)
152 0x0, # black -> black
154 0x4, # d.blue -> blue
155 0x5, # purple -> magenta
156 0x2, # d.green -> green
157 0x7, # gray 1 -> white
158 0xc, # m.blue -> intense blue
159 0xd, # l.blue -> intense magenta
160 0x3, # brown -> yellow
161 0xb, # orange -> intense yellow
162 0x7, # grey 2 -> white
163 0xd, # pink -> intense magenta
164 0xa, # l.green -> intense green
165 0xb, # yellow -> intense yellow
166 0xe, # aqua -> intense cyan
167 0xf, # white -> intense white
172 fd_in = sys.stdin.fileno()
173 fd_out = sys.stdout.fileno()
174 poll_in = select.poll()
175 poll_in.register(fd_in, select.POLLIN)
183 0x300: 0, # tone period
187 gr_mem = [[0 for j in range(40)] for i in range(40)]
192 format = alsaaudio.PCM_FORMAT_U8,
193 periodsize = PCM_PERIODSIZE
199 if attr is None and os.isatty(fd_in):
200 attr = termios.tcgetattr(fd_in)
201 atexit.register(deinit)
203 write('\x1b[?25l\x1b[0m') # hide cursor, reset attributes
209 write('\x1b[?25h\x1b[0m') # show cursor, reset attributes
210 termios.tcsetattr(fd_in, termios.TCSADRAIN, attr)
211 atexit.unregister(deinit)
221 return len(poll_in.poll(1)) != 0
224 return str(os.read(fd_in, 1), 'utf-8')
227 os.write(fd_out, bytes(data, 'utf-8'))
232 tone(1000, 100) # 1 kHz for .1 sec
234 # apple treats \r as \r\n, so if you write e.g. \r\n you'll get \r\n\n
244 elif mem[ZP_CH] < 39:
255 raise Exception('end of input') # due to piping or input redirection
257 raise Exception('user break')
263 write('\x1b[?25h') # show cursor
276 write('\x1b[?25l') # hide cursor
288 write(f'\x1b[{x:d}G')
292 write(f'\x1b[{y:d}d')
299 write('\x1b[2J\x1b[H')
312 def tone(freq, dur): # Hz, ms
313 # doesn't seem to work on any linux console on my laptop
314 #write(f'\x1b7\x1b[10;{freq:d}]\x1b[11;{dur:d}]\x07\x1b8')
315 u = time.monotonic() + dur * .001
318 0xff if (2 * freq * i // PCM_RATE) & 1 else 0
319 for i in range(PCM_RATE * dur // 1000)
324 i += pcm.write(buf[i:])
338 return mem.get(addr, 0)
340 def poke(addr, data):
344 if addr == ZP_WNDTOP or addr == ZP_WNDBTM:
345 # save cursor, set scrolling region, restore cursor
346 write(f'\x1b[s\x1b{mem[ZP_WNDTOP + 1]:d};{mem[ZP_WNDBTM]:d}24r\x1b[u')
350 if addr == 0x302 or addr == 0x310:
351 # for lemonade, see test/lemonade_tone_patched.py
352 a = mem[0x301] # duration_count
353 b = a / (((mem[0x300] - 1) & 0xff) + 1) # duration_count / period_count
354 cycles = 1.37788799e-02 - 4.21513128e-06 * a + 1.27999925e+02 * b
355 duration = 1.27361219e-05 + 2.50702246e-03 * a + 2.50310997e-03 * b
356 tone(round(cycles / duration), round(duration * 1e3))
358 # for lemonade, lighting (setup)
361 # for lemonade, lightning (execute)
363 elif addr == ROM_CLREOP:
365 elif addr == ROM_BELL2:
366 tone(1000, 100) # 1 kHz for .1 sec
368 raise Exception(f'call {addr:04x}')
371 # save cursor, set scrolling region, restore cursor
372 write('\x1b[s\x1b1;24r\x1b[u')
379 # clear screen, set scrolling region (homes cursor)
387 gr_mem[i][:] = [0 for j in range(40)]
389 def gr_update(x0, y0, x1, y1):
390 write('\x1b[s') # save cursor
394 for i in range(y0, y1):
395 write(f'\x1b[{i + 1:d};{x0 * 2 + 1:d}H')
396 for j in range(x0, x1):
397 new_bg = colors[gr_mem[i * 2][j]]
398 new_fg = colors[gr_mem[i * 2 + 1][j]]
399 # we cannot draw two different intense colours in same block,
400 # so just do the best we can (the greater colour gets priority)
403 elif new_bg > new_fg:
404 new_bg, new_fg = new_fg, new_bg
407 # we try to draw not-intense pixels using background, it
408 # might play nicer with terminals that use black-on-white
417 if new_bg != bg or new_fg != fg or new_br != br:
419 '\x1b[0{0:s}{1:s}{2:s}m'.format(
420 f';4{new_bg:d}' if new_bg else '',
421 f';3{new_fg:d}' if new_fg else '',
422 ';1' if new_br else ''
429 write('\x1b[u\x1b[0m') # restore cursor and attrs
437 gr_mem[y][x] = gr_color
439 gr_update(x, y, x + 1, y + 1)
444 for x in range(x0, x1 + 1):
445 gr_mem[y][x] = gr_color
447 gr_update(x0, y, x1 + 1, y + 1)
452 for y in range(y0, y1 + 1):
453 gr_mem[y][x] = gr_color
454 gr_update(x, y0 >> 1, x + 1, (y1 >> 1) + 1)
468 if __name__ == '__main__':
471 while not read_ready():
474 write(f'k {k:02x}\r')
478 # tone(int(round(220 * 2 ** (i / 12.))), 250)