Shop OBEX P1 Docs P2 Docs Learn Events
Audio Delay — Parallax Forums

Audio Delay

geokonstgeokonst Posts: 48
edited 2008-01-06 04:28 in Propeller 1
Dear all,
I would like your opinion on something. I am trying to make a prop audio delay. I came up with the following
CON
  _clkmode = xtal1 + pll16x
  _xinfreq = 5_000_000

OBJ

  adc : "Dual_ADC"
  vp    : "ViewportConduit_20"  
VAR
  long buffer [noparse][[/noparse]39]
  long out

PUB start | inc 

'INITIALISATION
adc.start (8,24,9,26,11)

'SHIFT REGISTER
repeat
    buffer:=adc.read1
    repeat inc from 1 to 38
      buffer[noparse][[/noparse]inc+1] := buffer[noparse][[/noparse]inc] 
    out:=buffer[noparse][[/noparse]39]



Which is practically a 11bit 39steps shift register. My question is: Is there a better way to do this? I am not confident about the timing of the shifting for i loop. I believe it works ok but I am not that experienced.
Another thing; Is the delay 1ms? The sampling frequency is 39Khz for 11bits. Is my program oversampling the adc object?

Finally, I get a DC offset of more than 1.5V with the mic circuit and approx 0,8 with the proposed ADC circuit. Anyone know why is that? Would a decoupling cap fix this or is it a softADC thing?

Thank you in advance.

Comments

  • geokonstgeokonst Posts: 48
    edited 2008-01-04 15:40
    Oh and I've read the previous audio delay posts but all attempts were in assembly which still scares me.
  • Nick MuellerNick Mueller Posts: 815
    edited 2008-01-04 16:02
    > My question is: Is there a better way to do this?

    Yes, a circular buffer. Thus you avoid the waste of time shifting the buffer around.
    A circular buffer has two pointers (indizes) into it. One for writing and one for reading. When the pointer reached the end, he simply wraps around. By increasing the difference between write and read-pointer, you can adjust the delay.

    Nick

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    Never use force, just go for a bigger hammer!

    The DIY Digital-Readout for mills, lathes etc.:
    YADRO
  • hippyhippy Posts: 1,981
    edited 2008-01-04 17:33
    Is the delay 1ms?

    You'd have to time it, but most likely not. You are buffering through a 39 stage delay, so the delay is 39 x sample rate. If you are sampling once every 25uS that's around 1ms delay.

    The sampling frequency is 39Khz for 11bits.

    To get 39kHz you need to take a sample every 25us. Spin is almost certainly too slow to do this which is why other programs you have looked at have been written in Assembler.
  • deSilvadeSilva Posts: 2,967
    edited 2008-01-04 17:46
    Timing:
    The REPEAT loop as given will take around 1.1 ms thus slowing the acquisition to 900 Samples = 450 Hz smile.gif
    The usual implementation of a queue by a ring buffer as explained by Nick will run in about 40 µs, adding the Dual- ADC access and some output for a DAC adding the same time again, takes 80 µs = 12000 samples = 6 kHz Thats quite telephone or AM radio - not so bad I should say.
  • geokonstgeokonst Posts: 48
    edited 2008-01-04 18:13
    hippie: isn't 39 * (1/39000) = 1ms? For the aquisition I am using dualADC which is in assembly and says it does 11bit for 39Khz

    deSilva: 1.1 ms!!! sounds awfull. I checked with viewport and the waveform looked ok. Would the sampling frequency be higher than 12kHz using assembly?
  • deSilvadeSilva Posts: 2,967
    edited 2008-01-04 18:29
    The Dual ADC is sampling @ 39kHz smile.gif But when no-one is using that values they will get lost of course.

    Beau has posted a very short "echo program" some time ago using exactly your strategy.

    At 39 kHz = 25µs you can run 500 machine instructions, that will be enough for some basic signal processing
  • geokonstgeokonst Posts: 48
    edited 2008-01-04 18:49
    Yes that makes sense. To be honest I though that the loop would take almost negligible time and thus oversample the adc (which would be the bottleneck) and ruin the timing not the other way round.
  • Nick MuellerNick Mueller Posts: 815
    edited 2008-01-04 19:19
    > adding the Dual- ADC access and some output for a DAC adding the same time again, takes 80 µs = 12000 samples = 6 kHz

    But then, what are the remaining 7 cogs for?


    Nick

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    Never use force, just go for a bigger hammer!

    The DIY Digital-Readout for mills, lathes etc.:
    YADRO
  • geokonstgeokonst Posts: 48
    edited 2008-01-04 19:36
    If I got it right it's ADC ACCESS and not the actual dual ADC that runs on another cog. right?


    to give you an insight:
    I want to do dual adc but with one channel delayed at any time. This is of course part of the convolution I am trying to do for soooo long [noparse]:)[/noparse]. Beau should make his code into an object (with bits and delay time as inputs) in obex as I beleive it's extremely useful (more than mic2headphones).
  • Ym2413aYm2413a Posts: 630
    edited 2008-01-04 19:47
    The problem with the above code is that the loop doesn't run at the same rate as the sampling rate.
    This means samples could be missed since the loop is slower then one sample duration.

    It'll work, but your output sampling rate is different then what is coming in. This also generates aliasing niose at the differential Nyquist frequency.

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    Share the knowledge: propeller.wikispaces.com
    Lets make some music: www.andrewarsenault.com/hss

  • Nick MuellerNick Mueller Posts: 815
    edited 2008-01-04 19:53
    I don't know how your hardware looks like, but:

    THINK COGS!

    If the ADC and DAC aren't hooked to the same I2C-bus, you could do the following:
    The ADC-cog loops through

    repeat
      repeat
      until FlagStartRead
      FlagStartRead:= false
      LocalADCValue:= ReadADC
      repeat
      until FlagSetValue
      FlagSetValue:= false
      TheGlobalADCValue:= LocalADCValue
    
    



    The DAC-cog:
    repeat
      repeat
      until FlagWriteDAC
      FlagWriteDAC:= false
      WriteDAC(TheGlobalDACValue)
    
    



    Then you have a third COG that sets the signals for the ADC-cog and the DAC-cog, does all the necessary delays and is writing to and reading from his ring buffer getting the value through TheGlobalADCValue and setting it through TheGlobalDACValue

    HTH,
    Nick, voting for more cogs!

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    Never use force, just go for a bigger hammer!

    The DIY Digital-Readout for mills, lathes etc.:
    YADRO

    Post Edited (Nick Mueller) : 1/4/2008 8:00:10 PM GMT
  • deSilvadeSilva Posts: 2,967
    edited 2008-01-04 20:33
    Nick that is EXACTLY what geokonst had done smile.gif
    Dual-ADC is the frist COG you describe (really nice code from Raymond Allen!)
    And - granted - your second COG is still missing; it only says
    out:=buffer[noparse][[/noparse] 39 ]
    at the moment..
  • geokonstgeokonst Posts: 48
    edited 2008-01-04 20:44
    And also I don't need a DAC.

    after the delay it will be something like
    out:=buffer[noparse][[/noparse]39] + adc.read2 'Where the second microphone is connected

    which for various delays will give a convolution (remember that) of two signals.

    Since deSilva's first answer I've been trying to modify the dualADC to do delay using Beau's code with (offcourse) no results. At this point I would like to thank you all for your answers so far and for Raymond's excellent and easy to use code.
  • VIRANDVIRAND Posts: 656
    edited 2008-01-04 23:11
    I may be late or missing something but for just a delay or echo a circular buffer can work with just one pointer
    something like this, always FIFO:

    repeat while delay_on
    __index:=(index+1)//buffer_size
    __digital_out:=BYTE[noparse][[/noparse]buffer+index]
    __BYTE[noparse][[/noparse]buffer+index]:=digital_in

    If you do use two indexes it gives effects such as changing voice pitch if they add more than one.
    If one of two indexes counts backwards it sounds like playing backwards or evil voice of doom.
  • Fred HawkinsFred Hawkins Posts: 997
    edited 2008-01-05 00:02
    Chip Gracey's StereoSpatializer.spin holds some advice on delays. It's in your parallax prop root folder.
  • Sleazy - GSleazy - G Posts: 79
    edited 2008-01-05 10:21
    Well, if you want any signifigant delay at good bit depth and sample rate, like a tape delay would give you, you could always use a circular buffer like mentioned in this thread. but if youd like some realtime selectability of delay range you could·buffer to an SD card ·.· I dont know the stream rates from SD, but someone got video to stream, so it should be good enough.

    If you try to do delay with just props hub or cog ram, you wouldnt be able to instantaneously change the RANGE of delay for more than the amount of sample time that the registers took to fully load up in the first place, without losing samples.· Most tape delays are just·realtime static delay,·but if you wanted to innovate, or do extreme pitch shifting,·SD would be the way to go.· Or wait for prop·II. whatever.

    Post Edited (Sleazy - G) : 1/5/2008 10:27:05 AM GMT
  • deSilvadeSilva Posts: 2,967
    edited 2008-01-05 10:38
    @ 40 kBy/sec you can obviously buffer around 1/2 sec in the HUB

    SD cards can be written @ 100kBy/s with is fast enough at the first glance However you need alternating reading AND writing for a delay algorithm with caching, which will make the algorithm more complex, and double the needed bandwidth!

    This looks like an application perfect for external FRAM, accessed via SPI.
  • Nick MuellerNick Mueller Posts: 815
    edited 2008-01-05 10:43
    > Nick that is EXACTLY what geokonst had done smile.gif

    Exactly?
    What he does is a blocking read.
    My scetch is following more the principles of multi-threaded programming.


    Nick

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    Never use force, just go for a bigger hammer!

    The DIY Digital-Readout for mills, lathes etc.:
    YADRO
  • deSilvadeSilva Posts: 2,967
    edited 2008-01-05 10:58
    Nick Mueller said...
    > What he does is a blocking read.
    It's not "blocking". The ADC is happily sampling in a separate COG.
    The main issue with SPIN is the missing transformation throughput ("transformation" = "copy", just a special case smile.gif ) Even ringbuffer has too much overhead for 40k Byte/sec.
  • geokonstgeokonst Posts: 48
    edited 2008-01-05 12:35
    I need a maximum of one milisecond. An sd card would be an overkill. As for changing the delay-if swiping doesnt work- I could have different delays runing in different cogs.

    Unfortunately I can't wait for PropII. I have to prove within 2 months that a 10quid chip can do things others use fpgas for.

    Post Edited (geokonst) : 1/5/2008 12:40:18 PM GMT
  • Nick MuellerNick Mueller Posts: 815
    edited 2008-01-05 12:43
    > It's not "blocking". The ADC is happily sampling in a separate COG.

    Sorry, I was just eyeballing because I didn't have the code. :-/
    Parallax-search doesn't find "Dual_ADC", Google search finds the Dual_ADC.spin, but not this thread.

    > Even ringbuffer has too much overhead for 40k Byte/sec.

    I get 16.5kHz out of it, with a simulated call to the ADC and an "out:= RingBuffer[noparse][[/noparse] someindex ]" Plus it is running in a counted repeat that eats some ticks too.


    Nick

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    Never use force, just go for a bigger hammer!

    The DIY Digital-Readout for mills, lathes etc.:
    YADRO
  • geokonstgeokonst Posts: 48
    edited 2008-01-05 14:37
    Has anyone written any circullar/ring buffer code in SPIN? deSilva is the 40us an estimate for a 40stage buffer or a measurement?
    Thanks
  • hippyhippy Posts: 1,981
    edited 2008-01-05 14:53
    geokonst said...
    I have to prove within 2 months that a 10quid chip can do things others use fpgas for.

    You also have to accept that it may not, or not in the way you are currently trying to do it. My recommendations would be -

    1) Get to grips with assembly language (PASM) programming. Sampling every 25uS (40kHz) should be achievable.

    2) Create a simple ADC to DAC 'copy' program which replicates output from input. Get that working as required.

    3) Add the delay functionality. You only need to hold 40 samples for a 1mS delay which can be held in Cog memory or Hub memory.

    Personally, I think (3) is the easiest part, especially once you've completed (1) and (2). I have no idea how complicated implementing ADC and DAC will be, but it appears there is already an ADC 'reference design' which should make that easier.

    Learning to use PASM is an unknown quantity, but the sooner you start, the sooner you'll become experienced. A 25uS loop is quite a long period for a 20 MIPS processor so you don't have the difficult task of a project which requires extensively optimised coding from the start. Even for an experienced assembly programmer PASM's necessary use of self-modifying code is an initial hurdle to get over. I would say it has taken me between one and two months to become what I'd consider proficient in PASM. I expect the learning curve would be shorter if entirely focused on that or with a good aptitude for the Propeller architecture.

    I believe what you are trying to do should be achievable within two months.
  • deSilvadeSilva Posts: 2,967
    edited 2008-01-05 15:09
    Nick Mueller said...
    >I get 16.5kHz out of it, with a simulated call to the ADC and an "out:= RingBuffer[noparse][[/noparse] someindex ]"
    This fits fine with my initial estimation of 12 kHz. Complementing the "out:=" stuff will most likely bring us down to that.

    But this is of no relevance. It should just show again the limits of SPIN in this specific area.
    SPIN can be used for some (not all) feasibility studies @ 10kHz
  • hippyhippy Posts: 1,981
    edited 2008-01-05 15:11
    geokonst said...
    Has anyone written any circullar/ring buffer code in SPIN?

    Untested, but here's one version ...

    VAR
      long ringBuffer[noparse][[/noparse] 64 ]
      long putPtr
      long getPtr
    
    PUB Main
      putPtr := 0
      getPtr := 40
      repeat
        ringBuffer[noparse][[/noparse] putPtr++ ] := ReadAdc
        putPtr &= $3F
        WriteDac( ringBuffer[noparse][[/noparse] getPtr++ ] )
        getPtr &= $3F
        WaitForTick
    
    PRI ReadAdc : adc
      adc := ?
    
    PRI WriteDac( dac )
      ? := dac
    
    PRI WaitForTick
    
    
    



    An equivalent, also untested, in PASM ...

    DAT             org     $000
    
    RingBuffer      jmp     #Start
    
                    long    0[noparse][[/noparse] 63 ]
    
    Start           call    #ReadAdc
    
    PutInBuffer     mov     RingBuffer,adc
                    add     PutInBuffer,k_0000_0200
                    andn    PutInBuffer,k_0003_8000
    
    GetFromBuffer   mov     dac,RingBuffer+40
                    add     GetFromBuffer,#1
                    andn    GetFromBuffer,#$1C0
    
                    call    #WriteDac
    
                    call    #WaitForTick
    
                    jmp     #Start
    
    ReadAdc         nop
    ReadAdc_Ret     ret
    
    WriteDac        nop
    WriteDac_Ret    ret
    
    WaitForTick     nop
    WaitForTick_Ret ret
    
    k_0000_0200     long    $0000_0200      ' ---- ---- ---- --dd dddd ddD- ---- ----
    k_0003_8000     long    $0003_8000      ' ---- ---- ---- --XX Xddd ddD- ---- ----
    
    adc             res     1
    dac             res     1
    
    
    
  • Nick MuellerNick Mueller Posts: 815
    edited 2008-01-05 15:55
    > This fits fine with my initial estimation of 12 kHz. Complementing the "out:=" stuff will most likely bring us down to that.

    No, I had that in the code too.
    The "trick" to get it that fast is to make the circular buffer sized to powers of 2 and not to make a MOD for the index, but to mask the index after the increment

    For geokonst:
    CON
      cBuffersize = 1024 'only power of two!
      cIndexMask = cBuffersize - 1
    
    VAR
      long ReadIndex
      long WriteIndex
      byte Buffer[noparse][[/noparse] cBuffersize ]
    
    PUB DoTheRingBuffer
      ReadIndex:= 0
      WriteIndex:= 100  'adjust to the delay you want
    
      repeat
        Buffer[noparse][[/noparse] WriteIndex++ ]:= GetADCValue
        WriteIndex&= cIndexMask
        out:= Buffer[noparse][[/noparse] ReadIndex++ ]
        ReadIndex&= cIndexMask
    
    



    It doesn't make a difference wether you use long, word or byte.

    HTH,
    Nick

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    Never use force, just go for a bigger hammer!

    The DIY Digital-Readout for mills, lathes etc.:
    YADRO
  • deSilvadeSilva Posts: 2,967
    edited 2008-01-06 00:34
    As there is no need to limit the absolute value of "WriteIndex" you can even get a further kHz out of:
    Buffer[noparse][[/noparse] cIndexMask & WriteIndex++ ]:= GetADCValue
    




    ---
    Edit:
    @Hippy: Nice tricky use of COG space!

    Post Edited (deSilva) : 1/6/2008 12:48:21 AM GMT
  • geokonstgeokonst Posts: 48
    edited 2008-01-06 04:28
    Nick, hippy, deSilva -> OMG thanks a lot guys. I am going to play with your codes tomorrow. I really owe you a great deal.

    Nick-> Sorry I don't get it. Why a power of 2?

    hippy -> It all started with great ambition. None has done sound source localization of continuous sounds based on anything smaller than an fpga before (at least I couldn’t find anything). When I saw the propeller I thought what an amazing chip! And so I started. Soon I got very disappointed when I got into the demands of such a system. I reached a point when I though it was not possible.Fpga would be so much easier. But then I saw stereospatialiser by Chip (which is almost what I want to do. imagine feeding the first adc to the stereospat and then adding the second adc to the left or right (depending on angle) output) and became optimistic again.
    So yes in general I was prepared to accept it is not possible, but I now strongly believe it is.
Sign In or Register to comment.