This function implements a pitch shifter. It takes mono in on the left channel, and outputs a mono signal to both the left and the right channels. The rotary encoder (MOD2) adjusts the pitch shift amount, and the pot (MOD1) adjusts the buffer size (and resultant delay).
The easiest way to change the pitch of a signal, is to play back the samples at a faster or slower rate, although this introduces two problems. The first is that if you are sending out data at a rate different from the rate at which you are receiving data, you will eventually run out of data to send out. This is called a buffer over-run or under-run, depending upon whether you're going faster or slower, and hitting the top or bottom of the buffer (the data stored in SRAM). The second problem is that the data is sampled at discreet points in time, and if you want a playback speed that is not a multiple of this time, you will need data from somewhere between those sample periods.
There are a number of options for how to deal with buffer boundaries, but the main issue which we are trying to overcome, is the sharp transition as you go from one end of the buffer to the other, and the data is no longer consistent. This creates an audible click in the sample playback. In this case, we are using a fading method. This involves having two samples playing back simultaneously, each from a different point in the buffer (spaced a half-buffer's distance from each other). As one sample gets closer to the boundary, its volume is faded down, and the other is faded up. This continues as each sample moves forward in the buffer, with the volume of the sample being determined by its distance from the buffer boundary. This gives very smooth transitions across the buffer boundary, but also has a slight reverb effect, as multiple delayed signals are being mixed together.
The most common method of dealing with the second problem (fractional sample rates) is interpolation. Interpolation is a method of guessing what a value might have been if we actually had sampled at that point in time. For this pitch-shifter function, we use a linear interpolation. This means we draw a straight line between the two adjacent samples from where we want data, and assume our value is on that line. So if we're closer in time to one sample versus the other, than our output value is closer in value to that sample (the output is a sum of the two values, weighted by their distance to our sample point).
The pot (MOD1) varies the buffer size used for sample playback, from 12ms to 1.5s. Smaller buffer sizes give a more accurate pitch-shifting effect, as you are playing through only very small samples at a time, and do not hear much of a delay. If the buffer is too small, you begin to hear the rate at which you are moving through the buffer, almost like a slight tremolo. For very large buffer sizes, it becomes a pitch shifted delay, which can be interesting when used with feedback, as each time the signal gets fed back in, its pitch is shifted again, causing an ever rising tone.
The rotary encoder (MOD2) varies the pitch shift amount, from -1 octave to +1 octave, in 12 chromatic steps each direction. In this way, the output can always be made to be "in tune" with the original signal. Rotations to the right increase the pitch, and vice versa. To maintain the correct pitch shift amount, a look-up table in program memory is used to store the precalculated values for each step.
; program: pitch_shifter-16b-fading.asm
; UID = 000039 - this is a unique id so variables dont conflict
; 16b address space (1.5s sample time)
; mono data in on left channel, mono out on both left and right
; rotary encoder (MOD2) controlled playback speed
; pot (MOD1) controlled buffer size
; program overview
;
; data is sent out and taken in from the codec. data is taken in on the
; left channel, and played out on both left and right. a buffer of the
; past n seconds is kept and the output is the result of sampling this
; buffer at varying playback speeds. the speed at which it plays through
; the memory is controlled by the rotary encoder (MOD2). turning the
; encoder to the right speeds playback up, and turning it the left slows
; playback down. this playback speed is limited to chromatic steps by
; using a lookup table in program memory to determine playback speed. the
; audio is kept clean over fractional sample periods by interpolating
; between the two closest samples. the output is a mix of the current
; sample, and a sample from the opposite side of the buffer, with the
; relative mix being determined by the distance to the buffer boundary.
; in this way, the audio is faded down as it crosses the buffer boundary.
; the pot (MOD1) controls the buffer size. the adc samples the pot 256
; times and deadbands the signal to remove glitches.
; constant definitions
;
.equ buffer_min_000039 = $0200 ; minimum buffer size
.equ delay_mem_000039 = $0200 ; memory position for desired delay time
;
;.equ step-12_000039 = $0080 ; these are the playback speeds used
;.equ step-11_000039 = $0088 ; they are stored in program memory
;.equ step-10_000039 = $0090 ; and not used here
;.equ step-9_000039 = $0098
;.equ step-8_000039 = $00A1
;.equ step-7_000039 = $00AB
;.equ step-6_000039 = $00B5
;.equ step-5_000039 = $00C0
;.equ step-4_000039 = $00CB
;.equ step-3_000039 = $00D7
;.equ step-2_000039 = $00E4
;.equ step-1_000039 = $00F2
;.equ step00_000039 = $0100
;.equ step01_000039 = $010F
;.equ step02_000039 = $011F
;.equ step03_000039 = $0130
;.equ step04_000039 = $0143
;.equ step05_000039 = $0156
;.equ step06_000039 = $016A
;.equ step07_000039 = $0180
;.equ step08_000039 = $0196
;.equ step09_000039 = $01AF
;.equ step10_000039 = $01C8
;.equ step11_000039 = $01E3
;.equ step12_000039 = $0200
; register usage - may be redefined in other sections
;
; r0 multiply result lsb
; r1 multiply result msb
; r2 sample 3/4 lsb
; r3 sample 3/4 msb
; r4 left/right lsb out
; r5 left/right msb out
; r6 left lsb in/temporary swap register
; r7 left msb in/temporary swap register
; r8 rotary encoder position counter
; r9 adc msb accumulator
; r10 adc fractional byte accumulator
; r11 adc lsb accumulator
; r12 playback speed increment lsb value ($0100 is normal speed)
; r13 playback speed increment msb value
; r14 rotary encoder counter
; r15 switch\adc counter
; r16 temporary swap register
; r17 temporary swap register
; r18 signed multiply register
; r19 signed multiply register
; r20 unsigned multiply register
; r21 unsigned multiply register
; r22 write address third byte/null register
; r23 read address fractional byte
; r24 write address lsb
; r25 write address msb
; r26 buffer length lsb
; r27 buffer length msb
; r28 read address lsb
; r29 read address msb
; r30 jump location for interrupt lsb
; r31 jump location for interrupt msb
; t rotary encoder edge indicator
;program starts here first time
; intialize registers
ldi r30,$29 ; set jump location to program start
clr r24 ; clear write register
clr r25
ldi r22,$00 ; setup write address high byte
clr r18 ; setup r18 as null register for carry addition and ddr setting
ldi r17,$ff ; setup r17 for ddr setting
clear_000039: ; clear delay buffer
; eliminates static when first switching to the delay setting
adiw r25:r24,$01 ; increment write register
adc r22,r18 ; increment write third byte
cpi r22,$01 ; check if 16b memory space has been cleared
breq cleardone_000039 ; continue until end of buffer reached
out portd,r24 ; set address
sts porth,r25
out portg,r22 ; pull ce low,we low,and set high bits of address
out ddra,r17 ; set porta as output for data write
out ddrc,r17 ; set portc as output for data write
out porta,r18 ; set data
out portc,r18 ; r18 is cleared above
sbi portg,portg2 ; pull we high to write
out ddra,r18 ; set porta as input for data lines
out ddrc,r18 ; set portc as input for data lines
rjmp clear_000039 ; continue clearing
cleardone_000039: ; reset registers
ldi r24,$00 ; initialize write register
ldi r25,$00
clr r22 ; setup null register
ldi r28,$00 ; set read address to minimum delay
ldi r29,$fd
clr r4 ; initialize data output registers
clr r5
ldi r26,$00 ; initialize buffer size
ldi r27,$06
sts delay_mem_000039,r26 ; store desired buffer size
sts (delay_mem_000039 + 1),r27 ; i ran out of registers
clr r12 ; initialize playback speed
ldi r16,$01
mov r13,r16
ldi r16,$0c ; initialize playback speed pointer
mov r8,r16
reti ; return and wait for next interrupt
;program begins here every time but first
; initiate data transfer to codec
sbi portb,portb0 ; toggle slave select pin
out spdr,r5 ; send out left channel msb
cbi portb,portb0
;increment write address
adiw r25:r24,$01 ; increment write address
cp r24,r26 ; check if at end of buffer
cpc r25,r27
brlo wait1_000039 ; do nothing if not at end of buffer
clr r24 ; reset buffer to bottom
clr r25
wait1_000039: ; check if byte has been sent
in r17,spsr
sbrs r17,spif
rjmp wait1_000039
in r7,spdr ; recieve in left channel msb
out spdr,r4 ; send out left channel lsb
;increment read address
add r23,r12 ; increment read register
adc r28,r13
adc r29,r22 ; r22 is cleared above
cp r28,r26 ; check if at end of buffer
cpc r29,r27
brlo wait2_000039 ; do nothing if not at end of buffer
clr r28 ; reset buffer to bottom
clr r29
wait2_000039: ; check if byte has been sent
in r17,spsr
sbrs r17,spif
rjmp wait2_000039
in r6,spdr ; recieve in left channel lsb
out spdr,r5 ; send out right channel msb
;write left channel data to sram
out portd,r24 ; set address
sts porth,r25
out portg,r22 ; pull ce low,we low,and set high bits of address
ldi r17,$ff
out ddra,r17 ; set porta as output for data write
out ddrc,r17 ; set portc as output for data write
out porta,r6 ; set data
out portc,r7
sbi portg,portg2 ; pull we high to write
out ddra,r22 ; set porta as input for data lines
out ddrc,r22 ; set portc as input for data lines
wait3_000039: ; check if byte has been sent
in r17,spsr
sbrs r17,spif
rjmp wait3_000039
in r17,spdr ; recieve in right channel msb
out spdr,r4 ; send out right channel lsb
;get left channel sample 1 data from sram
movw r17:r16,r29:r28 ; move read address to temporary register
out portd,r16 ; set address
sts porth,r17
ldi r21,$01 ; increment read address
add r16,r21 ; placed here to use 2 cycle wait
adc r17,r22 ; r22 is cleared above
in r6,pina ; get data
in r18,pinc ; get data
cp r16,r26 ; check if at end of buffer
cpc r17,r27
brlo wait4_000039 ; do nothing if not at end of buffer
clr r16 ; reset buffer to bottom
clr r17
wait4_000039: ; check if byte has been sent
in r19,spsr
sbrs r19,spif
rjmp wait4_000039
in r19,spdr ; recieve in right channel lsb
;get left channel sample 2 data from sram
out portd,r16 ; set address
sts porth,r17
nop ; wait 2 cycle setup time
nop
in r7,pina ; get data
in r19,pinc ; get data
;multiply sample 1 by distance
mov r20,r23 ; get distance from sample 1
com r20
mulsu r18,r20 ; (signed)Ah * (unsigned)B
movw r5:r4,r1:r0
mul r6,r20 ; (unsigned)Al * (unsigned)B
add r4,r1
adc r5,r22 ; r22 is cleared above
mov r17,r0
;multiply and accumulate sample 2 by distance
mulsu r19,r23 ; (signed)Ah * (unsigned)B
add r4,r0 ; accumulate result
adc r5,r1
mul r7,r23 ; (unsigned)Al * (unsigned)B
add r17,r0 ; accumulate result
adc r4,r1
adc r5,r22 ; r22 is cleared above
;get sample from other side of buffer
movw r17:r16,r29:r28 ; move current position to temporary register
movw r7:r6,r27:r26 ; move buffer size to temporary register
lsr r7 ; divide buffer size by 2
ror r6
cp r16,r6 ; check if in lower or upper half of buffer
cpc r17,r7
brsh buffer_flip_000039 ; subtract half buffer if in upper half
add r16,r6 ; add half buffer size if in lower half
adc r17,r7
rjmp getsample3_000039 ; continue
buffer_flip_000039: ; adjust to opposite side of memory
sub r16,r6 ; subtract half buffer size if in upper half
sbc r17,r7
getsample3_000039: ;get left channel sample 3 data from sram
out portd,r16 ; set address
sts porth,r17
add r16,r21 ; increment read address - r21 set to $01 above
adc r17,r22 ; r22 is cleared above
in r6,pina ; get data
in r18,pinc ; get data
cp r16,r26 ; check if at end of buffer
cpc r17,r27
brlo getsample4_000039 ; do nothing if not at end of buffer
clr r16 ; reset buffer to bottom
clr r17
getsample4_000039: ;get left channel sample 4 data from sram
out portd,r16 ; set address
sts porth,r17
nop ; wait 2 cycle setup time
nop
in r7,pina ; get data
in r19,pinc ; get data
;multiply sample 3 by distance
mulsu r18,r20 ; (signed)ah * b
movw r3:r2,r1:r0
mul r6,r20 ; al * b
add r2,r1
adc r3,r22 ; r22 is cleared above
mov r17,r0
;multiply sample 4 by distance
mulsu r19,r23 ; (signed)ah * b
add r2,r0 ; accumulate result
adc r3,r1
mul r7,r23 ; al * b
add r17,r0 ; accumulate result
adc r2,r1
adc r3,r22 ; r22 is cleared above
;get distance to boundary
movw r17:r16,r29:r28 ; move read address to temporary register
mov r18,r23
sub r16,r24 ; find distance to loop boundary
sbc r17,r25
brcc half_000039 ; check if result is negative
com r16 ; invert distance if negative
com r17
com r18
add r18,r21 ; r21 set to $01 above
adc r16,r22 ; r22 cleared above
adc r17,r22
half_000039: ; check if result is greater than half the buffer size
movw r7:r6,r27:r26 ; move buffer size to temporary register
lsr r7 ; divide buffer size by 2
ror r6
cp r16,r6 ; check if result is greater than half the buffer size
cpc r17,r7
brlo scale_000039 ; skip flip if not
sub r16,r26 ; flip result around boundary
sbc r17,r27
com r16
com r17
com r18
add r18,r21 ; r21 set to $01 above
adc r16,r22
adc r17,r22
scale_000039: ; scale distance to match buffer size - 50% accurate
movw r7:r6,r27:r26 ; move buffer size to temporary register
sbrc r7,$07 ; check if msb of buffer size is set
rjmp attenuate_000039 ; attenuate signal if 16b value
shift_000039: ; shift buffer size till it occupies full 16b
lsl r6 ; multiply buffer size by 2
rol r7
lsl r18 ; multiply distance by 2
rol r16
rol r17
sbrs r7,$07 ; check if msb of buffer size is set
rjmp shift_000039 ; keep checking if not set
attenuate_000039: ; multiply sample 1/2 by distance
lsl r18 ; multiply distance by 2 since max value is 1/2 buffer size
rol r16
rol r17
sub r6,r16 ; find complementary distance of sample 3/4
sbc r7,r17 ; only 1 bit error for not subtracting r18 as well
movw r21:r20,r7:r6 ; move distance to signed multiply register
movw r19:r18,r5:r4 ; move value to signed multiply register
mulsu r19,r17 ; (signed)ah * bh
movw r5:r4,r1:r0
mul r18,r16 ; al * bl
movw r7:r6,r1:r0
mulsu r19,r16 ; (signed)ah * bl
sbc r5,r22 ; r22 is cleared above
add r7,r0
adc r4,r1
adc r5,r22
mul r17,r18 ; bh * al
add r7,r0
adc r4,r1
adc r5,r22
;multiply and accumulate sample 3/4 with result from above
movw r19:r18,r3:r2 ; move value to signed multiply register
mulsu r19,r21 ; (signed)ah * bh
add r4,r0 ; accumulate result
adc r5,r1
mul r18,r20 ; al * bl
add r6,r0 ; accumulate result
adc r7,r1
adc r4,r22 ; r22 is cleared above
adc r5,r22
mulsu r19,r20 ; (signed)ah * bl
sbc r5,r22 ; accumulate result
add r7,r0
adc r4,r1
adc r5,r22
mul r21,r18 ; bh * al
add r7,r0
adc r4,r1
adc r5,r22
rotary_000039: ; check rotary encoder and adjust playback rate
; rotary encoder is externally debounced, so that is not done here.
; pin1 is sampled on a transition from high to low on pin0. if pin1 is
; high, a left turn occured, if pin1 is low, a right turn occured.
dec r14 ; reduce the sampling rate to help with debounce
brne check_000039 ; continue if not ready yet
ldi r17,$40 ; adjust sample frequency to catch all rising edges (1.5ms)
mov r14,r17
lds r17,pinj ; get switch data
sbrs r17,$00 ; check if pin0 is low
rjmp edge_000039 ; check if pin0 was low on previous sample
clt ; clear state register if back high
rjmp check_000039 ; finish off
edge_000039: ; check for falling edge
brts check_000039 ; do nothing if the edge was already detected
set ; set state register to indicate a falling edge occured
sbrs r17,$01 ; check if pin1 is high
rjmp increment_000039 ; increment playback if right rotation
ldi r16,$01 ; check if pitch at min
cp r8,r16
brlo check_000039 ; do nothing it at bottom
dec r8 ; decrement rotary encoder position counter
movw r17:r16,z ; store z register
ldi zh,$4c ; setup z pointer to fetch tone from lookup table
mov zl,r8
lsl zl
lpm r12,z+ ; move tone to pitch register
lpm r13,z
movw z,r17:r16 ; restore z register
rjmp check_000039 ; finish off
increment_000039: ; increment playback speed
ldi r16,$18 ; check if pitch at max
cp r8,r16
brsh reset1_000039 ; do nothing if at max already
inc r8 ; increment rotary encoder position counter
movw r17:r16,z ; store z register
ldi zh,$4c ; setup z pointer to fetch tone from lookup table
mov zl,r8
lsl zl
lpm r12,z+ ; move tone to pitch register
lpm r13,z
movw z,r17:r16 ; restore z register
rjmp check_000039 ; finish off
reset1_000039: ; reset tone register in case it goes too high
mov r8,r16 ; set tone register to max
check_000039: ; check if buffer size is correct
lds r16,delay_mem_000039 ; fetch desired buffer size
lds r17,(delay_mem_000039 + 1) ; i ran out of registers
cp r26,r16 ; compare current delay to desired delay
cpc r27,r17
brlo upcount_000039 ; increment if smaller than
breq adcsample_000039 ; do nothing if they are same size
sbiw r27:r26,$02 ; decrement buffer size
rjmp adcsample_000039 ; finish off
upcount_000039: ; increment buffer size register
adiw r27:r26,$02 ; increment buffer size
adcsample_000039: ; get loop setting
lds r17,adcsra ; get adc control register
sbrs r17,adif ; check if adc conversion is complete
rjmp done_000039 ; skip adc sampling
lds r16,adcl ; get low byte adc value
lds r17,adch ; get high byte adc value
add r10,r16 ; accumulate adc samples
adc r11,r17
adc r9,r22 ; r22 is cleared above
ldi r17,$f7
sts adcsra,r17 ; clear interrupt flag
dec r15 ; countdown adc sample clock
brne done_000039 ; move adc value to loop setting after 256 samples
lsr r9 ; divide accumulated value by 4 to make a 16b value
ror r11
ror r10
lsr r9
ror r11
ror r10
ldi r16,low(buffer_min_000039) ; fetch min buffer size
ldi r17,high(buffer_min_000039)
cp r10,r16 ; compare adc value to min buffer size
cpc r11,r17
brsh compare_000039 ; skip if above minimum buffer size
movw r11:r10,r17:r16 ; else set to minimum buffer size
compare_000039: ; compare to previous value
lds r16,delay_mem_000039 ; fetch desired delay time
lds r17,(delay_mem_000039 + 1) ; i ran out of registers
sub r16,r10 ; find difference between adc value and desired buffer size
sbc r17,r11
brcc deadband_000039 ; check for magnitude of change if positive
neg r16 ; else invert difference if negative
adc r17,r22 ; r22 is cleared above
neg r17
deadband_000039: ; see if pot has moved or if its just noise
cpi r16,$40 ; see if difference is greater than 1 lsb
cpc r17,r22 ; r22 is cleared above
brlo nochange_000039 ; dont update loop time if difference is not large enough
ldi r16,$fe ; make sure buffer size is even
and r10,r16
sts delay_mem_000039,r10 ; store new desired buffer size
sts (delay_mem_000039 + 1),r11 ; i ran out of registers
nochange_000039: ; clear accumulation registers
clr r10 ; empty accumulation registers
clr r11
clr r9
switchsample_000039: ; check rotary switch state
lds r16,pinj ; get switch data
andi r16,$78 ; mask off rotary switch
lsr r16 ; adjust switch position to program memory location
lsr r16
ldi r17,$02
add r16,r17
cpse r16,r31 ; check if location has changed
clr r30 ; reset jump register to intial state
mov r31,r16
done_000039:
reti ; return to waiting
Last edited 2010-08-21 by guest · 5 revisions