Propeller chip + SPDIF = ???

So, kind of on a random whim, I decided to build a software SPDIF encoder for the Propeller chip. It was an interesting challenge, and the results are pretty cool, but now I have no idea what do do with it. Not an uncommon problem for me, I guess…

Maybe I’ll give my Propeller board an Ethernet port and build a Squeezebox-on-a-chip.

Update: Ok, so maybe that’s not such a crazy idea.. (or is it?) Anyway, I just found this, and I had to order a couple:

[Ethernet Interface Board – ENC28J60]

Now if only there was a simple way to capture all sound output from a Windows/Linux/Mac machine and stream it over the network… The ESD protocol doesn’t count :P



{{

spdifOut v1.0 2008/07/26
-------------------------

This is an object for the Parallax Propeller, implementing an
S/PDIF (digital audio) output driver. We stream PCM data from
hub memory to a single output pin, which can be connected to
a suitable output driver (for coax) or to an LED (for Toslink
optical).

The audio source can be written in either Spin or assembly.
The input buffer is flexible: you supply a start address and
buffer size, this object provides a sample counter for flow
control purposes.

A buffer size of 1 and no counter can be used if you want the
SPDIF object to just poll a single hub memory location for
audio data. If you want flow control, you can use the counter
to test when the current sample has been output. You can use
this system to implement a full lockless FIFO queue, or a
double-buffering scheme- whichever method works best for your
application.

We use the Propeller's video output engine to generate the
S/PDIF signal. This means our output frequency is as flexible
as the Propeller's counter PLLs, and we can generate very high
output data rates. It also means that not all pins can be used
as S/PDIF outputs. Pins 3, 7, 11, 15, 19, 23, 27, and 31 can't
be used, as they are reserved for the aural subcarrier.

The S/PDIF bit rate is 64x the sample rate (2 subframes, each
with 32 bit positions). We need two video clocks per bit, for
the biphase mark encoding. This means our final video "pixel"
clock needs to be 128x the sampleHz rate. At 48kHz, this gives
us a "pixel" clock of 6.144 MHz. To get into the right frequency
range for our PLL, we run the PLL at 16x this frequency. For
48 KHz, this is a PLL frequency of 98 MHz. So, our PLL frequency
is always 2048x the audio sample rate, and we use a 16x divisor. 

References:
  http://en.wikipedia.org/wiki/S/PDIF
  http://en.wikipedia.org/wiki/AES/EBU
  http://en.wikipedia.org/wiki/Biphase_mark_code
  http://www.ebu.ch/CMSimages/en/tec_AES-EBU_eg_tcm6-11890.pdf
  http://www.epanorama.net/documents/audio/spdif.html
  http://www.users.qwest.net/~kmaxon/page/side/control61_137.htm
  http://opencores.org/projects.cgi/web/spdif_interface/overview
  
┌───────────────────────────────────┐
│ Copyright (c) 2008 Micah Dowty    │               
│ See end of file for terms of use. │
└───────────────────────────────────┘

}}

CON
  ' Channel status bits

  PROFESSIONAL_MODE  = 1 << 0   ' Is this professional or consumer audio?
  NON_AUDIO          = 1 << 1   ' Set if this is not regular PCM audio.
  ALLOW_COPIES       = 1 << 2   ' Set if no copy protection is enforced.
  PRE_EMPHASIS       = 1 << 3   ' Audio pre-emphasis enabled?
  PRE_RECORDED_DATA  = 1 << 15  ' Generation status. Is this pre-recorded data?
  HZ_44100           = 0 << 24
  HZ_OTHER           = 1 << 24
  HZ_48000           = 2 << 24
  HZ_32000           = 3 << 24

  DEFAULT_STATUS     = ALLOW_COPIES | HZ_44100

VAR
  byte cog
  long counter

PUB start(pin) : okay
  '' Start an S/PDIF driver on the specified pin. Uses one cog,
  '' returns nonzero on success.
  ''
  '' You must call setBuffer() before start(), and you may optionally
  '' call setChannelStatus().

  param_pin := pin
  param_counter := @counter
  counter~

  okay := cog := 1 + cognew(@entry, 0)

PUB stop
  if cog > 0
    cogstop(cog - 1)
    cog~

PUB setChannelStatus(status)
  '' Set the first 32 channel status bits. This implicitly sets the
  '' sample rate. Must be called before start()

  param_status := status

PUB setBuffer(addr, size)
  '' Tell the S/PDIF driver to continuously read samples from a
  '' circular buffer of 'size' samples, starting at 'addr'.
  
  param_input := param_in_begin := addr
  param_in_end := addr + (size << 2)

PUB getCountAddr
  '' Get the address of a counter long, which increments immediately after
  '' the driver cog reads a new sample from the buffer. This address can
  '' be used by other assembly-language cogs for fine-grained flow control.
  '' To find the driver's current position in the sample buffer, take this
  '' value modulo the buffer size.

  return  @counter

PUB getCount
  '' Return the number of samples that have been processed.

  return counter
    
DAT

'==============================================================================
' Driver Cog
'==============================================================================

                        org

                        '======================================================
                        ' Initialization
                        '======================================================

entry
                        mov     r1, d44100              ' Decode sample rate, in hz
                        test    param_status, hz_bit1 wz
              if_nz     mov     r1, d48000
                        test    param_status, hz_bit0 wz
              if_nz     mov     r1, d32000
                        
                        shl     r1, #7                  ' 128x CTRA * 16x PLL = 2048x
                        rdlong  r2, #0                  ' Read system CLKFREQ
                        mov     r0, #32+1               ' r3 = 2^32 * r1 / r2
:divLoop                cmpsub  r1, r2 wc
                        rcl     r3, #1
                        shl     r1, #1
                        djnz    r0, #:divLoop
                        mov     frqa, r3                ' Program CTRA frequency
                        movi    ctra, #%00001_011       ' PLL mode, divide by 16 (128x sample rate)

                        mov     vscl, init_vscl         ' Init VScl for one clock per 'pixel'               

                        mov     r0, #1                  ' Calculate DIRA
                        shl     r0, param_pin
                        mov     dira, r0
                        
                        mov     r0, param_pin           ' Calculate VPins
                        and     r0, #%111
                        mov     r1, #1
                        shl     r1, r0
                        mov     vcfg, r1

                        mov     r0, param_pin           ' Calculate VGroup
                        shr     r0, #3
                        movd    vcfg, r0

                        test    param_pin, #4 wz        ' Which half of the pin group are we in?
              if_z      movi    vcfg, #%010_000_000     '   Baseband on 3:0
              if_nz     movi    vcfg, #%011_000_000     '   Baseband on 7:4

                        '======================================================
                        ' Main loop
                        '======================================================

mainLoop
                        ' Begin a new data block
                        mov     preamble_left, preamble_b

                        ' First channel status word, settable by the caller.

                        mov     channel_status, param_status
                        call    #genFrames32

                        ' Other bits are reserved, unused for consumer audio.
                        ' There are a total of 192 bits (6 words) 

                        mov     channel_status, #0
                        call    #genFrames32
                        mov     channel_status, #0
                        call    #genFrames32
                        mov     channel_status, #0
                        call    #genFrames32
                        mov     channel_status, #0
                        call    #genFrames32
                        mov     channel_status, #0
                        call    #genFrames32

                        jmp     #mainLoop


                        '======================================================
                        ' Frame generator
                        '======================================================

                        ' This outputs a group of 32 frames. Each frame gets
                        ' one bit of channel status data from the "channel_status"
                        ' shift register.
                        '
                        ' Note that both subframes in a frame get the same bit
                        ' of channel status data.

genFrames32
                        mov     frame, #32
:loop
                        
                        rdlong  sample, param_input         ' Read audio sample from hub memory                 
                        add     param_input, #4             ' Next sample
                        add     counter_reg, #1             ' Update sample counter, wrap buffer
                        wrlong  counter_reg, param_counter
                        cmp     param_input, param_in_end nr,wz
              if_z      mov     param_input, param_in_begin

                        mov     subframe, sample            ' Left channel (high word) to subframe bits 23:8
                        shr     subframe, #16
                        shl     subframe, #8

                        rcr     channel_status, #1 wc, nr   ' Insert channel status and parity
                        muxc    subframe, status_mask wc    '   Note that the parity bit should cause the entire
                        muxc    subframe, parity_mask       '   biphase-encoded subframe to have an even number of
                                                            '   bit transitions, so each subframe should end at the
                                                            '   same logic level.                 
                        
                        mov     preamble, preamble_left    ' Output left subchannel
                        call    #bmEncode

                        mov     subframe, sample           ' Right channel (low word) to subframe bits 23:8
                        shl     subframe, #16
                        shr     subframe, #8

                        rcr     channel_status, #1 wc      ' Insert channel status and parity
                        muxc    subframe, status_mask wc
                        muxc    subframe, parity_mask                        

                        mov     preamble, preamble_w       ' Output right subchannel
                        call    #bmEncode
                        
                        ' Next frame..
                        mov     preamble_left, preamble_m
                        djnz    frame, #:loop

genFrames32_ret         ret 


                        '======================================================
                        ' Biphase Mark Encoder
                        '======================================================

bmEncode
                        ' Fill the biphase encoding buffer with the value of the
                        ' last cell we just output. If we just sent a '0', every bit
                        ' in the buffer will be a '0', etc. This provides continuity
                        ' from subframe to subframe. We never set bits explicitly in
                        ' the biphase buffer, we just introduce transitions using XOR.

                        rcl     biphase, #1 wc          ' Extract high bit (the last bit we sent)
                        muxc    biphase, hFFFFFFFF      ' Set all bits

                        ' Load the preamble. The preamble is not biphase encoded,
                        ' but it is subject to being inverted if the previous cell
                        ' was a 1. This step is omitted for the second half (second
                        ' 32 cells) of a subframe.
                        '
                        ' In biphase encoding, every bit unconditionally begins with
                        ' one transition. We can add these transitions too, in the same
                        ' operation.
                        '
                        ' The masks below select all cels in the biphase register that are
                        ' output after the bit we're currently encoding. Any time we
                        ' XOR the biphase register with the mask, we're creating a
                        ' transition on all future bits. The mask starts at the first
                        ' odd numbered non-preamble bit.

                        xor     biphase, preamble

                        ' To actually biphase encode our input data, we'll insert
                        ' additional transitions every time there's a 1 bit in our input.
                        ' For the first half of the subframe, we're processing 12 bits
                        ' of subframe data. (16, minus the 4-bit preamble)
                        '
                        ' The loop is unrolled, since this is very speed-critical. At
                        ' 48 KHz, we have less than three instructions per bit!

                        rcr     subframe, #1 wc         ' Extract the next LSB from the subframe
              if_nc     xor     biphase, mask_4         ' Insert a transition only for '1' bits.
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_5
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_6
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_7
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_8
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_9
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_10
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_11
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_12
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_13
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_14
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_15

                        waitvid palette, biphase        ' Output the first half of this subframe

                        ' Get ready to do that again.. this time we won't
                        ' output a preamble, but we'll output the next 16 bits
                        ' of the subframe data.

                        rcl     biphase, #1 wc
                        muxc    biphase, hFFFFFFFF
                        xor     biphase, h55555555

                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_0
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_1
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_2
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_3
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_4
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_5
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_6
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_7
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_8
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_9
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_10
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_11
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_12
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_13
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_14
                        rcr     subframe, #1 wc
              if_nc     xor     biphase, mask_15

                        waitvid palette, biphase        ' Output the second half of this subframe

bmEncode_ret            ret

                        
'------------------------------------------------------------------------------
' Initialized Data
'------------------------------------------------------------------------------

hFFFFFFFF               long    $FFFFFFFF
h55555555               long    $55555555
d44100                  long    44100
d48000                  long    48000
d32000                  long    32000
hz_bit0                 long    HZ_OTHER
hz_bit1                 long    HZ_48000

param_status            long    DEFAULT_STATUS
param_pin               long    0
param_input             long    0
param_in_begin          long    0
param_in_end            long    0
param_counter           long    0
counter_reg             long    0

' 1 clock per 'pixel', 32 clocks per 'frame'.
init_vscl               long    (1 << 12) | 32

' Our "palette". Since our actual output pin could be any of
' the three video D/A pins, and we run the video hardware in
' 1-bit mode, this is just an identity mapping that lets us use
' the video generator as a dumb shift register.

palette                 long    $07_00

' Subframe bits
status_mask             long    1 << (30 - 4)
parity_mask             long    1 << (31 - 4)
                        
' S/PDIF preambles. These are ordered LSB-first, ready for loading into
' 'biphase' before encoding the rest of a subframe.
'
' These are the preamble encodings that occur after a '0' bit. After a '1'
' bit, these preambles are inverted.
'
' All odd-numbered unused bits must be '1', so we can insert the fixed
' transitions in the same operation.

preamble_b              long    %010101010101010101010101_00010111
preamble_m              long    %010101010101010101010101_01000111
preamble_w              long    %010101010101010101010101_00100111

' For speed, we precalculate all XOR masks.

mask_0                  long    %11111111111111111111111111111110
mask_1                  long    %11111111111111111111111111111000
mask_2                  long    %11111111111111111111111111100000
mask_3                  long    %11111111111111111111111110000000
mask_4                  long    %11111111111111111111111000000000
mask_5                  long    %11111111111111111111100000000000
mask_6                  long    %11111111111111111110000000000000
mask_7                  long    %11111111111111111000000000000000
mask_8                  long    %11111111111111100000000000000000
mask_9                  long    %11111111111110000000000000000000
mask_10                 long    %11111111111000000000000000000000
mask_11                 long    %11111111100000000000000000000000
mask_12                 long    %11111110000000000000000000000000
mask_13                 long    %11111000000000000000000000000000
mask_14                 long    %11100000000000000000000000000000
mask_15                 long    %10000000000000000000000000000000


'------------------------------------------------------------------------------
' Uninitialized Data
'------------------------------------------------------------------------------

r0                      res     1
r1                      res     1
r2                      res     1
r3                      res     1

' This is the 32-bit S/PDIF subframe, before biphase encoding.  Bits 0-3, the
' preamble, are totally omitted from this packet. Bit 0 in this field is actually
' bit 4 in the standard S/PDIF subframe.
'
' The S/PDIF subframe format:
'
'  Bits 0-3:    Preamble (Not present, since it's inserted after biphase encoding)
'       4-7:    Auziliary audio data bits (for nonstandard 24-bit audio)
'       8-11:   Unused audio bits, for 20-bit sound
'       12-27:  16-bit audio sample
'       28:     Invalid frame bit (normally zero)
'       29:     Subcode data (for ASCII text, and other odd features)
'       30:     Channel status bit (see 'channel_status', above)
'       31:     Even parity, not including preamble

subframe                res     1

biphase                 res     1
preamble                res     1
preamble_left           res     1
frame                   res     1
sample                  res     1
channel_status          res     1

                        fit

{{
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                   TERMS OF USE: MIT License                                                  │                                                            
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation    │ 
│files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,    │
│modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software│
│is furnished to do so, subject to the following conditions:                                                                   │
│                                                                                                                              │
│The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.│
│                                                                                                                              │
│THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE          │
│WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR         │
│COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,   │
│ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                         │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
}}   

''
'' Simple demo for playing a WAV file via SPDIF, with double-buffering.
'' This demo is really dumb, and it doesn't understand WAV headers at all-
'' we just treat the whole file like it's a raw 16-bit signed stereo PCM
'' file at 44.1 KHz.
''
'' -- Micah Dowty <micah@navi.cx>
''

'' Configure these parameters for your particular setup:

CON
  _clkmode = xtal1 + pll16x
  _xinfreq = 5_000_000

  SPDIF_PIN     = 25
  SD_CARD_PIN   = 16

DAT
  filename byte "somerslt.wav", 0

OBJ
  sd : "fsrw"
  spdif : "spdifOut"

CON  
  BUFFER_SIZE = 128     ' Must be a power of two  

VAR
  long bufA[BUFFER_SIZE]
  long bufB[BUFFER_SIZE]
  
PUB main | f, c

  if sd.mount(SD_CARD_PIN)
    panic
  if sd.popen(@filename, "r")
    panic

  spdif.setBuffer(@bufA, constant(BUFFER_SIZE * 2))
  spdif.start(SPDIF_PIN)
  
  repeat
    ' Wait until the driver is using bufB, then read bufA
    repeat until spdif.getCount & BUFFER_SIZE
    sd.pread(@bufA, constant(BUFFER_SIZE * 4))

    ' Now the opposite...
    repeat while spdif.getCount & BUFFER_SIZE
    sd.pread(@bufB, constant(BUFFER_SIZE * 4))

PUB panic
  '' If an SD card error occurs, blink the SPDIF output LED forever.
  
  dira[SPDIF_PIN]~~
  repeat
    waitcnt(clkfreq/10 + cnt)
    !outa[SPDIF_PIN]

git clone https://git.approximate.life/navi-misc.git