PitchShifterChromatic

Chromatic Pitch Shifter

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.

pitchshifter_chromatic.asm


   1 ; program: pitch_shifter-16b-fading.asm
   2 ; UID = 000039 - this is a unique id so variables dont conflict
   3 ; 16b address space (1.5s sample time)
   4 ; mono data in on left channel, mono out on both left and right
   5 ; rotary encoder (MOD2) controlled playback speed
   6 ; pot (MOD1) controlled buffer size
   7 
   8 ; program overview
   9 ;
  10 ; data is sent out and taken in from the codec.  data is taken in on the
  11 ; left channel, and played out on both left and right.  a buffer of the
  12 ; past n seconds is kept and the output is the result of sampling this
  13 ; buffer at varying playback speeds. the speed at which it plays through
  14 ; the memory is controlled by the rotary encoder (MOD2).  turning the
  15 ; encoder to the right speeds playback up, and turning it the left slows
  16 ; playback down.  this playback speed is limited to chromatic steps by
  17 ; using a lookup table in program memory to determine playback speed.  the
  18 ; audio is kept clean over fractional sample periods by interpolating
  19 ; between the two closest samples.  the output is a mix of the current
  20 ; sample, and a sample from the opposite side of the buffer, with the
  21 ; relative mix being determined by the distance to the buffer boundary.
  22 ; in this way, the audio is faded down as it crosses the buffer boundary.
  23 ; the pot (MOD1) controls the buffer size.  the adc samples the pot 256
  24 ; times and deadbands the signal to remove glitches.
  25 
  26 ; constant definitions
  27 ;
  28 .equ buffer_min_000039 = $0200 ; minimum buffer size
  29 .equ delay_mem_000039 = $0200 ; memory position for desired delay time
  30 ;
  31 ;.equ step-12_000039 = $0080 ; these are the playback speeds used
  32 ;.equ step-11_000039 = $0088 ; they are stored in program memory
  33 ;.equ step-10_000039 = $0090 ; and not used here
  34 ;.equ step-9_000039 = $0098
  35 ;.equ step-8_000039 = $00A1
  36 ;.equ step-7_000039 = $00AB
  37 ;.equ step-6_000039 = $00B5
  38 ;.equ step-5_000039 = $00C0
  39 ;.equ step-4_000039 = $00CB
  40 ;.equ step-3_000039 = $00D7
  41 ;.equ step-2_000039 = $00E4
  42 ;.equ step-1_000039 = $00F2
  43 ;.equ step00_000039 = $0100
  44 ;.equ step01_000039 = $010F
  45 ;.equ step02_000039 = $011F
  46 ;.equ step03_000039 = $0130
  47 ;.equ step04_000039 = $0143
  48 ;.equ step05_000039 = $0156
  49 ;.equ step06_000039 = $016A
  50 ;.equ step07_000039 = $0180
  51 ;.equ step08_000039 = $0196
  52 ;.equ step09_000039 = $01AF
  53 ;.equ step10_000039 = $01C8
  54 ;.equ step11_000039 = $01E3
  55 ;.equ step12_000039 = $0200
  56 
  57 ; register usage - may be redefined in other sections
  58 ;
  59 ; r0  multiply result lsb
  60 ; r1  multiply result msb
  61 ; r2  sample 3/4 lsb
  62 ; r3  sample 3/4 msb
  63 ; r4  left/right lsb out
  64 ; r5  left/right msb out
  65 ; r6  left lsb in/temporary swap register
  66 ; r7  left msb in/temporary swap register
  67 ; r8  rotary encoder position counter
  68 ; r9  adc msb accumulator
  69 ; r10 adc fractional byte accumulator
  70 ; r11 adc lsb accumulator
  71 ; r12 playback speed increment lsb value ($0100 is normal speed)
  72 ; r13 playback speed increment msb value
  73 ; r14 rotary encoder counter
  74 ; r15 switch\adc counter
  75 ; r16 temporary swap register
  76 ; r17 temporary swap register
  77 ; r18 signed multiply register
  78 ; r19 signed multiply register
  79 ; r20 unsigned multiply register
  80 ; r21 unsigned multiply register
  81 ; r22 write address third byte/null register
  82 ; r23 read address fractional byte
  83 ; r24 write address lsb
  84 ; r25 write address msb
  85 ; r26 buffer length lsb
  86 ; r27 buffer length msb
  87 ; r28 read address lsb
  88 ; r29 read address msb
  89 ; r30 jump location for interrupt lsb
  90 ; r31 jump location for interrupt msb
  91 ; t   rotary encoder edge indicator
  92 
  93 ;program starts here first time
  94 ; intialize registers
  95 ldi r30,$27 ; set jump location to program start
  96 clr r24 ; clear write register
  97 clr r25
  98 ldi r22,$00 ; setup write address high byte
  99 clr r18 ; setup r18 as null register for carry addition and ddr setting
 100 ldi r17,$ff ; setup r17 for ddr setting
 101 
 102 clear_000039: ; clear delay buffer
 103 ; eliminates static when first switching to the delay setting
 104 
 105 adiw r25:r24,$01 ; increment write register
 106 adc r22,r18 ; increment write third byte
 107 cpi r22,$01 ; check if 16b memory space has been cleared
 108 breq cleardone_000039 ; continue until end of buffer reached
 109 out portd,r24 ; set address
 110 sts porth,r25
 111 out portg,r22 ; pull ce low,we low,and set high bits of address
 112 out ddra,r17 ; set porta as output for data write
 113 out ddrc,r17 ; set portc as output for data write
 114 out porta,r18 ; set data
 115 out portc,r18 ; r18 is cleared above
 116 sbi portg,portg2 ; pull we high to write
 117 out ddra,r18 ; set porta as input for data lines
 118 out ddrc,r18 ; set portc as input for data lines
 119 rjmp clear_000039 ; continue clearing
 120 
 121 cleardone_000039: ; reset registers
 122 
 123 ldi r24,$00 ; initialize write register
 124 ldi r25,$00
 125 clr r22 ; setup null register
 126 ldi r28,$00 ; set read address to minimum delay
 127 ldi r29,$fd
 128 clr r4 ; initialize data output registers
 129 clr r5
 130 ldi r26,$00 ; initialize buffer size
 131 ldi r27,$06
 132 sts delay_mem_000039,r26 ; store desired buffer size
 133 sts (delay_mem_000039 + 1),r27 ; i ran out of registers
 134 clr r12 ; initialize playback speed
 135 ldi r16,$01
 136 mov r13,r16
 137 reti ; return and wait for next interrupt
 138 
 139 ;program begins here every time but first
 140 ; initiate data transfer to codec
 141 sbi portb,portb0 ; toggle slave select pin
 142 out spdr,r5 ; send out left channel msb
 143 cbi portb,portb0
 144 
 145 ;increment write address
 146 adiw r25:r24,$01 ; increment write address
 147 cp r24,r26 ; check if at end of buffer
 148 cpc r25,r27
 149 brlo wait1_000039 ; do nothing if not at end of buffer
 150 clr r24 ; reset buffer to bottom
 151 clr r25
 152 
 153 wait1_000039: ; check if byte has been sent
 154 
 155 in r17,spsr
 156 sbrs r17,spif
 157 rjmp wait1_000039
 158 in r7,spdr ; recieve in left channel msb
 159 out spdr,r4 ; send out left channel lsb
 160 
 161 ;increment read address
 162 add r23,r12 ; increment read register
 163 adc r28,r13
 164 adc r29,r22 ; r22 is cleared above
 165 cp r28,r26 ; check if at end of buffer
 166 cpc r29,r27
 167 brlo wait2_000039 ; do nothing if not at end of buffer
 168 clr r28 ; reset buffer to bottom
 169 clr r29
 170 
 171 wait2_000039: ; check if byte has been sent
 172 
 173 in r17,spsr
 174 sbrs r17,spif
 175 rjmp wait2_000039
 176 in r6,spdr ; recieve in left channel lsb
 177 out spdr,r5 ; send out right channel msb
 178 
 179 ;write left channel data to sram
 180 out portd,r24 ; set address
 181 sts porth,r25
 182 out portg,r22 ; pull ce low,we low,and set high bits of address
 183 ldi r17,$ff
 184 out ddra,r17 ; set porta as output for data write
 185 out ddrc,r17 ; set portc as output for data write
 186 out porta,r6 ; set data
 187 out portc,r7
 188 sbi portg,portg2 ; pull we high to write
 189 out ddra,r22 ; set porta as input for data lines
 190 out ddrc,r22 ; set portc as input for data lines
 191 
 192 wait3_000039: ; check if byte has been sent
 193 
 194 in r17,spsr
 195 sbrs r17,spif
 196 rjmp wait3_000039
 197 in r17,spdr ; recieve in right channel msb
 198 out spdr,r4 ; send out right channel lsb
 199 
 200 ;get left channel sample 1 data from sram
 201 movw r17:r16,r29:r28 ; move read address to temporary register
 202 out portd,r16 ; set address
 203 sts porth,r17
 204 ldi r21,$01 ; increment read address
 205 add r16,r21 ; placed here to use 2 cycle wait
 206 adc r17,r22 ; r22 is cleared above
 207 in r6,pina ; get data
 208 in r18,pinc ; get data
 209 cp r16,r26 ; check if at end of buffer
 210 cpc r17,r27
 211 brlo wait4_000039 ; do nothing if not at end of buffer
 212 clr r16 ; reset buffer to bottom
 213 clr r17
 214 
 215 wait4_000039: ; check if byte has been sent
 216 
 217 in r19,spsr
 218 sbrs r19,spif
 219 rjmp wait4_000039
 220 in r19,spdr ; recieve in right channel lsb
 221 
 222 ;get left channel sample 2 data from sram
 223 out portd,r16 ; set address
 224 sts porth,r17
 225 nop ; wait 2 cycle setup time
 226 nop
 227 in r7,pina ; get data
 228 in r19,pinc ; get data
 229 
 230 ;multiply sample 1 by distance
 231 mov r20,r23 ; get distance from sample 1
 232 com r20
 233 mulsu r18,r20 ; (signed)Ah * (unsigned)B
 234 movw r5:r4,r1:r0
 235 mul     r6,r20  ; (unsigned)Al * (unsigned)B
 236 add     r4,r1
 237 adc     r5,r22 ; r22 is cleared above
 238 mov r17,r0
 239 
 240 ;multiply and accumulate sample 2 by distance
 241 mulsu r19,r23 ; (signed)Ah * (unsigned)B
 242 add r4,r0 ; accumulate result
 243 adc r5,r1
 244 mul     r7,r23  ; (unsigned)Al * (unsigned)B
 245 add r17,r0 ; accumulate result
 246 adc     r4,r1
 247 adc     r5,r22 ; r22 is cleared above
 248 
 249 ;get sample from other side of buffer
 250 movw r17:r16,r29:r28 ; move current position to temporary register
 251 movw r7:r6,r27:r26 ; move buffer size to temporary register
 252 lsr r7 ; divide buffer size by 2
 253 ror r6
 254 cp r16,r6 ; check if in lower or upper half of buffer
 255 cpc r17,r7
 256 brsh buffer_flip_000039 ; subtract half buffer if in upper half
 257 add r16,r6 ; add half buffer size if in lower half
 258 adc r17,r7
 259 rjmp getsample3_000039 ; continue
 260 
 261 buffer_flip_000039: ; adjust to opposite side of memory
 262 
 263 sub r16,r6 ; subtract half buffer size if in upper half
 264 sbc r17,r7
 265 
 266 getsample3_000039: ;get left channel sample 3 data from sram
 267 
 268 out portd,r16 ; set address
 269 sts porth,r17
 270 add r16,r21 ; increment read address - r21 set to $01 above
 271 adc r17,r22 ; r22 is cleared above
 272 in r6,pina ; get data
 273 in r18,pinc ; get data
 274 cp r16,r26 ; check if at end of buffer
 275 cpc r17,r27
 276 brlo getsample4_000039 ; do nothing if not at end of buffer
 277 clr r16 ; reset buffer to bottom
 278 clr r17
 279 
 280 getsample4_000039: ;get left channel sample 4 data from sram
 281 
 282 out portd,r16 ; set address
 283 sts porth,r17
 284 nop ; wait 2 cycle setup time
 285 nop
 286 in r7,pina ; get data
 287 in r19,pinc ; get data
 288 
 289 ;multiply sample 3 by distance
 290 mulsu r18,r20 ; (signed)ah * b
 291 movw r3:r2,r1:r0
 292 mul     r6,r20 ; al * b
 293 add     r2,r1
 294 adc     r3,r22 ; r22 is cleared above
 295 mov r17,r0
 296 
 297 ;multiply sample 4 by distance
 298 mulsu r19,r23 ; (signed)ah * b
 299 add r2,r0 ; accumulate result
 300 adc r3,r1
 301 mul     r7,r23  ; al * b
 302 add r17,r0 ; accumulate result
 303 adc     r2,r1
 304 adc     r3,r22 ; r22 is cleared above
 305 
 306 ;get distance to boundary
 307 movw r17:r16,r29:r28 ; move read address to temporary register
 308 mov r18,r23
 309 sub r16,r24 ; find distance to loop boundary
 310 sbc r17,r25
 311 brcc half_000039 ; check if result is negative
 312 com r16 ; invert distance if negative
 313 com r17
 314 com r18
 315 add r18,r21 ; r21 set to $01 above
 316 adc r16,r22 ; r22 cleared above
 317 adc r17,r22
 318 
 319 half_000039: ; check if result is greater than half the buffer size
 320 
 321 movw r7:r6,r27:r26 ; move buffer size to temporary register
 322 lsr r7 ; divide buffer size by 2
 323 ror r6
 324 cp r16,r6 ; check if result is greater than half the buffer size
 325 cpc r17,r7
 326 brlo scale_000039 ; skip flip if not
 327 sub r16,r26 ; flip result around boundary
 328 sbc r17,r27
 329 com r16
 330 com r17
 331 com r18
 332 add r18,r21 ; r21 set to $01 above
 333 adc r16,r22
 334 adc r17,r22
 335 
 336 scale_000039: ; scale distance to match buffer size - 50% accurate
 337 
 338 movw r7:r6,r27:r26 ; move buffer size to temporary register
 339 sbrc r7,$07 ; check if msb of buffer size is set
 340 rjmp attenuate_000039 ; attenuate signal if 16b value
 341 
 342 shift_000039: ; shift buffer size till it occupies full 16b
 343 
 344 lsl r6 ; multiply buffer size by 2
 345 rol r7
 346 lsl r18 ; multiply distance by 2
 347 rol r16
 348 rol r17
 349 sbrs r7,$07 ; check if msb of buffer size is set
 350 rjmp shift_000039 ; keep checking if not set
 351 
 352 attenuate_000039: ; multiply sample 1/2 by distance
 353 
 354 lsl r18 ; multiply distance by 2 since max value is 1/2 buffer size
 355 rol r16
 356 rol r17
 357 sub r6,r16 ; find complementary distance of sample 3/4
 358 sbc r7,r17 ; only 1 bit error for not subtracting r18 as well
 359 movw r21:r20,r7:r6 ; move distance to signed multiply register
 360 movw r19:r18,r5:r4 ; move value to signed multiply register
 361 mulsu r19,r17 ; (signed)ah * bh
 362 movw r5:r4,r1:r0
 363 mul     r18,r16 ; al * bl
 364 movw r7:r6,r1:r0
 365 mulsu r19,r16 ; (signed)ah * bl
 366 sbc     r5,r22 ; r22 is cleared above
 367 add     r7,r0
 368 adc     r4,r1
 369 adc     r5,r22
 370 mul r17,r18 ; bh * al
 371 add     r7,r0
 372 adc     r4,r1
 373 adc     r5,r22
 374 
 375 ;multiply and accumulate sample 3/4 with result from above
 376 movw r19:r18,r3:r2 ; move value to signed multiply register
 377 mulsu r19,r21 ; (signed)ah * bh
 378 add     r4,r0 ; accumulate result
 379 adc     r5,r1
 380 mul     r18,r20 ; al * bl
 381 add     r6,r0 ; accumulate result
 382 adc     r7,r1
 383 adc     r4,r22 ; r22 is cleared above
 384 adc     r5,r22
 385 mulsu r19,r20 ; (signed)ah * bl
 386 sbc     r5,r22 ; accumulate result
 387 add     r7,r0
 388 adc     r4,r1
 389 adc     r5,r22
 390 mul r21,r18 ; bh * al
 391 add     r7,r0
 392 adc     r4,r1
 393 adc     r5,r22
 394 
 395 rotary_000039: ; check rotary encoder and adjust playback rate
 396 ; rotary encoder is externally debounced, so that is not done here.
 397 ; pin1 is sampled on a transition from high to low on pin0.  if pin1 is
 398 ; high, a left turn occured, if pin1 is low, a right turn occured.
 399 dec r14 ; reduce the sampling rate to help with debounce
 400 brne check_000039 ; continue if not ready yet
 401 ldi r17,$40 ; adjust sample frequency to catch all rising edges (1.5ms)
 402 mov r14,r17
 403 lds r17,pinj ; get switch data
 404 sbrs r17,$00 ; check if pin0 is low
 405 rjmp edge_000039 ; check if pin0 was low on previous sample
 406 clt ;  clear state register if back high
 407 rjmp check_000039 ; finish off
 408 
 409 edge_000039: ; check for falling edge
 410 
 411 brts check_000039 ; do nothing if the edge was already detected
 412 set ; set state register to indicate a falling edge occured
 413 sbrs r17,$01 ; check if pin1 is high
 414 rjmp increment_000039 ; increment playback if right rotation
 415 ldi r16,$01 ; check if pitch at min
 416 cp r8,r16
 417 brlo check_000039 ; do nothing it at bottom
 418 dec r8 ; decrement rotary encoder position counter
 419 movw r17:r16,z ; store z register
 420 ldi zh,$4c ; setup z pointer to fetch tone from lookup table
 421 mov zl,r8
 422 lsl zl
 423 lpm r12,z+ ; move tone to pitch register
 424 lpm r13,z
 425 movw z,r17:r16 ; restore z register
 426 rjmp check_000039 ; finish off
 427 
 428 increment_000039: ; increment playback speed
 429 
 430 ldi r16,$18 ; check if pitch at max
 431 cp r8,r16
 432 brsh reset1_000039 ; do nothing if at max already
 433 inc r8 ; increment rotary encoder position counter
 434 movw r17:r16,z ; store z register
 435 ldi zh,$4c ; setup z pointer to fetch tone from lookup table
 436 mov zl,r8
 437 lsl zl
 438 lpm r12,z+ ; move tone to pitch register
 439 lpm r13,z
 440 movw z,r17:r16 ; restore z register
 441 rjmp check_000039 ; finish off
 442 
 443 reset1_000039: ; reset tone register in case it goes too high
 444 
 445 ldi r16,$18 ; set tone register to max
 446 mov r8,r16
 447 
 448 check_000039: ; check if buffer size is correct
 449 
 450 lds r16,delay_mem_000039 ; fetch desired buffer size
 451 lds r17,(delay_mem_000039 + 1) ; i ran out of registers
 452 cp r26,r16 ; compare current delay to desired delay
 453 cpc r27,r17
 454 brlo upcount_000039 ; increment if smaller than
 455 breq adcsample_000039 ; do nothing if they are same size
 456 sbiw r27:r26,$02 ; decrement buffer size
 457 rjmp adcsample_000039 ; finish off
 458 
 459 upcount_000039: ; increment buffer size register
 460 
 461 adiw r27:r26,$02 ; increment buffer size
 462 
 463 adcsample_000039: ; get loop setting
 464 
 465 lds r17,adcsra ; get adc control register
 466 sbrs r17,adif ; check if adc conversion is complete
 467 rjmp done_000039 ; skip adc sampling
 468 lds r16,adcl ; get low byte adc value
 469 lds r17,adch ; get high byte adc value
 470 add r10,r16 ; accumulate adc samples
 471 adc r11,r17
 472 adc r9,r22 ; r22 is cleared above
 473 ldi r17,$f7
 474 sts adcsra,r17 ; clear interrupt flag
 475 dec r15 ; countdown adc sample clock
 476 brne done_000039 ; move adc value to loop setting after 256 samples
 477 lsr r9 ; divide accumulated value by 4 to make a 16b value
 478 ror r11
 479 ror r10
 480 lsr r9
 481 ror r11
 482 ror r10
 483 ldi r16,low(buffer_min_000039) ; fetch min buffer size
 484 ldi r17,high(buffer_min_000039)
 485 cp r10,r16 ; compare adc value to min buffer size
 486 cpc r11,r17
 487 brsh compare_000039 ; skip if above minimum buffer size
 488 movw r11:r10,r17:r16 ; else set to minimum buffer size
 489 
 490 compare_000039: ; compare to previous value
 491 
 492 lds r16,delay_mem_000039 ; fetch desired delay time
 493 lds r17,(delay_mem_000039 + 1) ; i ran out of registers
 494 sub r16,r10 ; find difference between adc value and desired buffer size
 495 sbc r17,r11
 496 brcc deadband_000039 ; check for magnitude of change if positive
 497 neg r16 ; else invert difference if negative
 498 adc r17,r22 ; r22 is cleared above
 499 neg r17
 500 
 501 deadband_000039: ; see if pot has moved or if its just noise
 502 
 503 cpi r16,$40 ; see if difference is greater than 1 lsb
 504 cpc r17,r22 ; r22 is cleared above
 505 brlo nochange_000039 ; dont update loop time if difference is not large enough
 506 ldi r16,$fe ; make sure buffer size is even
 507 and r10,r16
 508 sts delay_mem_000039,r10 ; store new desired buffer size
 509 sts (delay_mem_000039 + 1),r11 ; i ran out of registers
 510 
 511 nochange_000039: ; clear accumulation registers
 512 
 513 clr r10 ; empty accumulation registers
 514 clr r11
 515 clr r9
 516 
 517 switchsample_000039: ; check rotary switch state
 518 
 519 lds r16,pinj ; get switch data
 520 andi r16,$78 ; mask off rotary switch
 521 lsr r16 ; adjust switch position to program memory location
 522 lsr r16
 523 ldi r17,$02
 524 add r16,r17
 525 cpse r16,r31 ; check if location has changed
 526 clr r30 ; reset jump register to intial state
 527 mov r31,r16
 528 
 529 done_000039:
 530 
 531 reti ; return to waiting
 532 

PitchShifterChromatic (last edited 2010-08-13 21:45:09 by guest)