328 lines
9.3 KiB
ArmAsm
328 lines
9.3 KiB
ArmAsm
;
|
|
; player.s
|
|
;
|
|
; Copyright © 2020 Kris Kennaway. All rights reserved.
|
|
;
|
|
; Delta modulation audio player for streaming audio over Ethernet.
|
|
;
|
|
; The encoder works by modeling the Apple II speaker as an RLC circuit and encoding audio using delta modulation.
|
|
; The resulting audio stream causes the Apple II speaker to precisely trace out the desired waveform.
|
|
;
|
|
; When we tick the speaker it inverts the applied voltage across it, and the speaker responds by oscillating with
|
|
; an exponential decay towards the new level. By matching the speaker response parameters to actual hardware we can
|
|
; precisely model how the speaker will respond to voltage changes, and use this to make the speaker "trace out" our
|
|
; desired waveform. We can't do this precisely -- the speaker will zig-zag around the target waveform because we can
|
|
; only move it in finite steps -- so there is some left-over quantization noise that manifests as background static.
|
|
;
|
|
; This player is capable of manipulating the speaker with 1-cycle precision, i.e. a 1MHz sampling rate, depending on
|
|
; how the "player opcodes" are chained together by the ethernet bytestream. The catch is that once we have toggled
|
|
; the speaker we can't toggle it again until at least 10 cycles have passed, but we can pick any interval >= 10 cycles
|
|
;
|
|
; Some other tricks used here:
|
|
;
|
|
; - The minimal 10-cycle speaker loop is: STA $C030; JMP (WDATA), where we use an undocumented property of the
|
|
; Uthernet II: I/O registers on the WDATA don't wire up all of the address lines, so they are also accessible at
|
|
; other address offsets. In particular WDATA+1 is a duplicate copy of WMODE. In our case WMODE happens to be 0x3.
|
|
; This lets us use WDATA as a jump table into page 3, where we place our player code. We then choose the network
|
|
; byte stream to contain the low-order byte of the target address we want to jump to next.
|
|
; - As with my ][-Vision streaming video+audio player, we schedule a "slow path" dispatch to occur every 2KB in the
|
|
; byte stream, and use this to manage the socket buffers (ACK the read 2KB and wait until at least 2KB more is
|
|
; available, which is usually non-blocking). While doing this we need to maintain a regular (a, b) cadence so the
|
|
; speaker is in a known trajectory. We can compensate for this in the audio encoder.
|
|
;
|
|
; TODO: explain the two-stage dispatch for end-of-frame processing
|
|
|
|
.proc main
|
|
init:
|
|
JMP bootstrap
|
|
|
|
; TODO: make these configurable
|
|
SRCADDR: .byte 10,0,65,02 ; 10.0.65.02 W5100 IP
|
|
FADDR: .byte 10,0,0,1 ; 10.0.0.1 FOREIGN IP
|
|
FPORT: .byte $07,$b9 ; 1977 FOREIGN PORT
|
|
MAC: .byte $00,$08,$DC,$01,$02,$03 ; W5100 MAC ADDRESS
|
|
|
|
; SLOT 3 I/O ADDRESSES FOR THE W5100
|
|
; Change this to support the Uthernet II in another slot
|
|
;
|
|
; TODO: make slot I/O addresses customizable at runtime - would probably require somehow
|
|
; compiling a list of all of the binary offsets at which we reference $C0bx and patching
|
|
; them in memory or on-disk.
|
|
WMODE = $C0b4
|
|
WADRH = $C0b5
|
|
WADRL = $C0b6
|
|
WDATA = $C0b7
|
|
|
|
; W5100 LOCATIONS
|
|
MACADDR = $0009 ; MAC ADDRESS
|
|
SRCIP = $000F ; SOURCE IP ADDRESS
|
|
RMSR = $001A ; RECEIVE BUFFER SIZE
|
|
|
|
; SOCKET 0 LOCATIONS
|
|
S0MR = $0400 ; SOCKET 0 MODE REGISTER
|
|
S0CR = $0401 ; COMMAND REGISTER
|
|
S0SR = $0403 ; STATUS REGISTER
|
|
S0LOCALPORT = $0404 ; LOCAL PORT
|
|
S0FORADDR = $040C ; FOREIGN ADDRESS
|
|
S0FORPORT = $0410 ; FOREIGN PORT
|
|
S0TXRR = $0422 ; TX READ POINTER REGISTER
|
|
S0TXWR = $0424 ; TX WRITE POINTER REGISTER
|
|
S0RXRSR = $0426 ; RX RECEIVED SIZE REGISTER
|
|
S0RXRD = $0428 ; RX READ POINTER REGISTER
|
|
|
|
; SOCKET 0 PARAMETERS
|
|
RXBASE = $6000 ; SOCKET 0 RX BASE ADDR
|
|
RXMASK = $1FFF ; SOCKET 0 8KB ADDRESS MASK
|
|
TXBASE = $4000 ; SOCKET 0 TX BASE ADDR
|
|
TXMASK = RXMASK ; SOCKET 0 TX MASK
|
|
|
|
; SOCKET COMMANDS
|
|
SCOPEN = $01 ; OPEN
|
|
SCCONNECT = $04 ; CONNECT
|
|
SCDISCON = $08 ; DISCONNECT
|
|
SCSEND = $20 ; SEND
|
|
SCRECV = $40 ; RECV
|
|
|
|
; SOCKET STATUS
|
|
STINIT = $13
|
|
STESTABLISHED = $17
|
|
|
|
PRODOS = $BF00 ; ProDOS MLI entry point
|
|
RESET_VECTOR = $3F2 ; Reset vector
|
|
COUT = $FDED
|
|
HOME = $FC58
|
|
|
|
TICK = $C030 ; where the magic happens
|
|
TEXTOFF = $C050
|
|
FULLSCR = $C052
|
|
PAGE2OFF = $C054
|
|
PAGE2ON = $C055
|
|
ptr = $06 ; TODO: we only use this for connection retry count
|
|
zpdummy = $ff
|
|
|
|
; RESET AND CONFIGURE W5100
|
|
bootstrap:
|
|
; install reset handler
|
|
LDA #<real_exit
|
|
STA RESET_VECTOR
|
|
LDA #>real_exit
|
|
STA RESET_VECTOR+1
|
|
EOR #$A5
|
|
STA RESET_VECTOR+2 ; checksum to ensure warm-start reset
|
|
|
|
LDA #6 ; 5 RETRIES TO GET CONNECTION
|
|
STA ptr ; NUMBER OF RETRIES
|
|
|
|
reset_w5100:
|
|
LDA #$80 ; reset
|
|
STA WMODE
|
|
LDA #3 ; CONFIGURE WITH AUTO-INCREMENT
|
|
STA WMODE
|
|
|
|
; ASSIGN MAC ADDRESS
|
|
LDA #>MACADDR
|
|
STA WADRH
|
|
LDA #<MACADDR
|
|
STA WADRL
|
|
LDX #0
|
|
@L1:
|
|
LDA MAC,X
|
|
STA WDATA ; USING AUTO-INCREMENT
|
|
INX
|
|
CPX #6 ;COMPLETED?
|
|
BNE @L1
|
|
|
|
; ASSIGN A SOURCE IP ADDRESS
|
|
LDA #<SRCIP
|
|
STA WADRL
|
|
LDX #0
|
|
@L2:
|
|
LDA SRCADDR,X
|
|
STA WDATA
|
|
INX
|
|
CPX #4
|
|
BNE @L2
|
|
|
|
;CONFIGURE BUFFER SIZES
|
|
|
|
LDA #<RMSR
|
|
STA WADRL
|
|
LDA #3 ; 8KB TO SOCKET 0
|
|
STA WDATA ; SET RECEIVE BUFFER
|
|
STA WDATA ; SET TRANSMIT BUFFER
|
|
|
|
; CONFIGURE SOCKET 0 FOR TCP
|
|
|
|
LDA #>S0MR
|
|
STA WADRH
|
|
LDA #<S0MR
|
|
STA WADRL
|
|
LDA #$21 ; TCP MODE | !DELAYED_ACK
|
|
STA WDATA
|
|
|
|
; SET LOCAL PORT NUMBER
|
|
|
|
LDA #<S0LOCALPORT
|
|
STA WADRL
|
|
LDA #$C0 ; HIGH BYTE OF LOCAL PORT
|
|
STA WDATA
|
|
LDA #0 ; LOW BYTE
|
|
STA WDATA
|
|
|
|
; SET FOREIGN ADDRESS
|
|
LDA #<S0FORADDR
|
|
STA WADRL
|
|
LDX #0
|
|
@L3:
|
|
LDA FADDR,X
|
|
STA WDATA
|
|
INX
|
|
CPX #4
|
|
BNE @L3
|
|
|
|
; SET FOREIGN PORT
|
|
LDA FPORT ; HIGH BYTE OF FOREIGN PORT
|
|
STA WDATA ; ADDR PTR IS AT FOREIGN PORT
|
|
LDA FPORT+1 ; LOW BYTE OF PORT
|
|
STA WDATA
|
|
|
|
; OPEN SOCKET
|
|
LDA #<S0CR
|
|
STA WADRL
|
|
LDA #SCOPEN ;OPEN COMMAND
|
|
STA WDATA
|
|
|
|
; CHECK STATUS REGISTER TO SEE IF SUCCEEDED
|
|
LDA #<S0SR
|
|
STA WADRL
|
|
LDA WDATA
|
|
CMP #STINIT ; IS IT SOCK_INIT?
|
|
BEQ OPENED
|
|
LDY #0
|
|
@L4:
|
|
LDA @SOCKERR,Y
|
|
BEQ @LDONE
|
|
JSR COUT
|
|
INY
|
|
BNE @L4
|
|
@LDONE: BRK
|
|
@SOCKERR: .byte $d5,$d4,$c8,$c5,$d2,$ce,$c5,$d4,$a0,$c9,$c9,$ba,$a0,$c3,$cf,$d5,$cc,$c4,$a0,$ce,$cf,$d4,$a0,$cf,$d0,$c5,$ce,$a0,$d3,$cf,$c3,$cb,$c5,$d4,$a1
|
|
; "UTHERNET II: COULD NOT OPEN SOCKET!"
|
|
.byte $8D,$00 ; cr+null
|
|
|
|
; TCP SOCKET WAITING FOR NEXT COMMAND
|
|
OPENED:
|
|
LDA #<S0CR
|
|
STA WADRL
|
|
LDA #SCCONNECT
|
|
STA WDATA
|
|
|
|
; WAIT FOR TCP TO CONNECT AND BECOME ESTABLISHED
|
|
|
|
CHECKTEST:
|
|
LDA #<S0SR
|
|
STA WADRL
|
|
LDA WDATA ; GET SOCKET STATUS
|
|
BEQ FAILED ; 0 = SOCKET CLOSED, ERROR
|
|
CMP #STESTABLISHED
|
|
BEQ setup ; SUCCESS
|
|
BNE CHECKTEST
|
|
|
|
FAILED:
|
|
DEC ptr
|
|
BEQ ERRDONE ; TOO MANY FAILURES
|
|
LDA #$AE ; "."
|
|
JSR COUT
|
|
JMP reset_w5100 ; TRY AGAIN
|
|
|
|
ERRDONE:
|
|
LDY #0
|
|
@L:
|
|
LDA ERRMSG,Y
|
|
BEQ @DONE
|
|
JSR COUT
|
|
INY
|
|
BNE @L
|
|
@DONE: BRK
|
|
|
|
ERRMSG: .byte $d3,$cf,$c3,$cb,$c5,$d4,$a0,$c3,$cf,$d5,$cc,$c4,$a0,$ce,$cf,$d4,$a0,$c3,$cf,$ce,$ce,$c5,$c3,$d4,$a0,$ad,$a0,$c3,$c8,$c5,$c3,$cb,$a0,$d2,$c5,$cd,$cf,$d4,$c5,$a0,$c8,$cf,$d3,$d4
|
|
; "SOCKET COULD NOT CONNECT - CHECK REMOTE HOST"
|
|
.byte $8D,$00
|
|
|
|
setup:
|
|
; move player code into $3xx
|
|
LDX #0
|
|
@0:
|
|
LDA begin_copy_page3,X
|
|
STA $300,X
|
|
INX
|
|
CPX #(end_copy_page3 - begin_copy_page3+1)
|
|
BNE @0
|
|
|
|
; clear screen
|
|
jsr HOME
|
|
|
|
; We keep our own copy of the W5100 S0RXRD pointer because it's cheaper to maintain a local copy rather than
|
|
; asking the W5100 for it in end-of-frame processing.
|
|
lda #$00
|
|
sta RXRD
|
|
|
|
; Wait for socket buffer to have at least 2KB of data in it
|
|
LDX #>S0RXRSR
|
|
STX WADRH
|
|
LDX #<S0RXRSR
|
|
|
|
fill_socket:
|
|
LDA #$07 ; Wait for at least 8 pages i.e. 2KB of data
|
|
@0:
|
|
STX WADRL ; #<S0RXRSR
|
|
CMP WDATA ; Check high byte of received size
|
|
BCS @0 ; Loop if not enough
|
|
; There is data to read - we don't care exactly how much but it's at least 2K, which is enough to be sure we can
|
|
; process the entirety of the next frame.
|
|
;
|
|
; Update W5100 address to point to start of socket buffer
|
|
LDX #>RXBASE
|
|
STX WADRH
|
|
LDX #$00
|
|
STX WADRL
|
|
|
|
LDY #$31 ; establish required invariant for core audio loop
|
|
JMP (WDATA) ; Start playing!
|
|
|
|
real_exit:
|
|
INC RESET_VECTOR+2 ; Invalidate power-up byte
|
|
JSR PRODOS ; Call the MLI ($BF00)
|
|
.BYTE $65 ; CALL TYPE = QUIT
|
|
.ADDR exit_parmtable ; Pointer to parameter table
|
|
|
|
exit_parmtable:
|
|
.BYTE 4 ; Number of parameters is 4
|
|
.BYTE 0 ; 0 is the only quit type
|
|
.WORD 0000 ; Pointer reserved for future use
|
|
.BYTE 0 ; Byte reserved for future use
|
|
.WORD 0000 ; Pointer reserved for future use
|
|
|
|
; TODO: store this in ZP instead? Cheaper to access (3 cycles instead of 4) but this tends to make it harder to align
|
|
; the speaker accesses during EOF processing, since most opcodes we're using have an even cycle length
|
|
RXRD:
|
|
.byte 00
|
|
|
|
; Stage 2 and 3 player code
|
|
.include "player_stage2_3_generated.s"
|
|
|
|
; Stage 1 player code, which will be copied to $3xx for execution
|
|
begin_copy_page3:
|
|
; generated audio playback code
|
|
.include "player_generated.s"
|
|
|
|
; Quit to ProDOS
|
|
exit:
|
|
JMP real_exit
|
|
end_copy_page3:
|
|
|
|
.segment "DATA256"
|
|
.include "player_stage3_table_generated.s"
|
|
|
|
.endproc
|