Files
linguist/samples/Propeller Spin/VocalTract.spin

737 lines
49 KiB
Plaintext

{{
┌───────────────────────────────────────────┬────────────────┬───────────────────────────────────┬─────────────────┐
│ Vocal Tract v1.1 │ by Chip Gracey │ Copyright (c) 2006 Parallax, Inc. │ 28 October 2006 │
├───────────────────────────────────────────┴────────────────┴───────────────────────────────────┴─────────────────┤
│ │
│ This object synthesizes a human vocal tract in real-time. It requires one cog and at least 80 MHz. │
│ │
│ The vocal tract is controlled via 13 single-byte parameters which must reside in the parent object: │
│ │
│ VAR byte aa,ga,gp,vp,vr,f1,f2,f3,f4,na,nf,fa,ff 'vocal tract parameters │
│ │
│ │
│ aa │
│ ┌────────────┐ │
│ │ ASPIRATION ├──┐ │
│ └────────────┘ │ f1 f2 f3 f4 na nf │
│  ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌───────┐ │
│ +┣──┤ F1 ├──┤ F2 ├──┤ F3 ├──┤ F4 ├──┤ NASAL ├──┐ │
│ ga gp  └────┘ └────┘ └────┘ └────┘ └───────┘ │ │
│ ┌─────────┐ │  │
│ │ GLOTTAL ├──┘ +┣── OUTPUT │
│ └────┬────┘ fa ff  │
│  ┌───────────┐ │ │
│ vp │ vr │ FRICATION ├──┘ │
│ ┌────┴────┐ └───────────┘ │
│ │ VIBRATO │ │
│ └─────────┘ │
│ │
│ │
│ ┌───────────┬──────────────────────┬─────────────┬────────────────────────────────────────────────┐ │
│ │ parameter │ description │ unit │ notes │ │
│ ├───────────┼──────────────────────┼─────────────┼────────────────────────────────────────────────┤ │
│ │ aa │ aspiration amplitude │ 0..255 │ breath volume: silent..loud, linear │ │
│ │ ga │ glottal amplitude │ 0..255 │ voice volume: silent..loud, linear │ │
│ │ gp │ glottal pitch │ 1/48 octave │ voice pitch: 100 ─ 110.00Hz (musical note A2) │ │
│ │ vp │ vibrato pitch │ 1/48 octave │ voice vibrato pitch: 48 ─ ± 1/2 octave swing │ │
│ │ vr │ vibrato rate │ 0.0763 Hz │ voice vibrato rate: 52 ─ 4 Hz │ │
│ │ f1 │ formant1 frequency │ 19.53 Hz │ 1st resonator frequency: 40 ─ 781 Hz │ │
│ │ f2 │ formant2 frequency │ 19.53 Hz │ 2nd resonator frequency: 56 ─ 1094 Hz │ │
│ │ f3 │ formant3 frequency │ 19.53 Hz │ 3rd resonator frequency: 128 ─ 2500 Hz │ │
│ │ f4 │ formant4 frequency │ 19.53 Hz │ 4th resonator frequency: 179 ─ 3496 Hz │ │
│ │ na │ nasal amplitude │ 0..255 │ anti-resonator level: off..on, linear │ │
│ │ nf │ nasal frequency │ 19.53 Hz │ anti-resonator frequency: 102 ─ 1992 Hz │ │
│ │ fa │ frication amplitude │ 0..255 │ white noise volume: silent..loud, linear │ │
│ │ ff │ frication frequency │ 39.06 Hz │ white noise frequency: 60 ─ 2344 Hz ("Sh") │ │
│ └───────────┴──────────────────────┴─────────────┴────────────────────────────────────────────────┘ │
│ │
│ The parent object alternately modifies one or more of these parameters and then calls the go(time) method to │
│ queue the entire 13-parameter frame for feeding to the vocal tract. The vocal tract will load one queued frame │
│ after another and smoothly interpolate between them over specified amounts of time without interruption. Up to │
│ eight frames will be queued in order to relax the frame-generation timing requirement of the parent object. If │
│ eight frames are queued, the parent must then wait to queue another frame. If the vocal tract runs out of │
│ frames, it will continue generating samples based on the last frame. When a new frame is queued, it will │
│ immediately load it and begin inter-polating towards it. │
│ │
│ The vocal tract generates audio samples at a continuous rate of 20KHz. These samples can be output to pins via │
│ delta-modulation for RC filtering or direct transducer driving. An FM aural subcarrier can also be generated for │
│ inclusion into a TV broadcast controlled by another cog. Regardless of any output mode, samples are always │
│ streamed into a special variable so that other objects can access them in real-time. │
│ │
│ In order to achieve optimal sound quality, it is worthwhile to maximize amplitudes such as 'ga' to the point │
│ just shy of numerical overflow. Numerical overflow results in high-amplitude noise bursts which are quite │
│ disruptive. The closeness of 'f1'-'f4' and their relationship to 'gp' can greatly influence the amount of 'ga' │
│ that can be applied before overflow occurs. You must determine through experimentation what the limits are. By │
│ pushing 'ga' close to the overflow point, you will maximize the signal-to-noise ratio of the vocal tract, │
│ resulting in the highest quality sound. Once your vocal tract programming is complete, the attenuation level │
│ can then be used to reduce the overall output in 3dB steps while preserving the signal-to-noise ratio. │
│ │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Revision History v1.0 released 26 October 2006 │
│ │
│ v1.1 If the vocal tract runs out of frames, its internal parameters will now be brought all the way to the │
│ last frame's values. Before, they were left one interpolation point shy, and then set to the last frame's │
│ values at the start of the next frame. For continuous frames this was trivial, but it posed a problem │
│ during frame gaps because the internal parameters would get stalled at transition points just shy of the │
│ last frame's values. This change makes the vocal tract behave more sensibly during frame gaps. │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
}}
CON
frame_buffers = 8 'frame buffers (2n)
frame_bytes = 3 {for stepsize} + 13 {for aa..ff} '16 bytes per frame
frame_longs = frame_bytes / 4 '4 longs per frame
frame_buffer_bytes = frame_bytes * frame_buffers
frame_buffer_longs = frame_longs * frame_buffers
VAR
long cog, tract, pace
long index, attenuation, sample '3 longs ...must
long dira_, dirb_, ctra_, ctrb_, frqa_, cnt_ '6 longs ...be
long frames[frame_buffer_longs] 'many longs ...contiguous
PUB start(tract_ptr, pos_pin, neg_pin, fm_offset) : okay
'' Start vocal tract driver - starts a cog
'' returns false if no cog available
''
'' tract_ptr = pointer to vocal tract parameters (13 bytes)
'' pos_pin = positive delta-modulation pin (-1 to disable)
'' neg_pin = negative delta-modulation pin (pos_pin must also be enabled, -1 to disable)
'' fm_offset = offset frequency for fm aural subcarrier generation (-1 to disable, 4_500_000 for NTSC)
'Reset driver
stop
'Remember vocal tract parameters pointer
tract := tract_ptr
'Initialize pace to 100%
pace := 100
'If delta-modulation pin(s) enabled, ready output(s) and ready ctrb for duty mode
if pos_pin > -1
dira_[pos_pin >> 5 & 1] |= |< pos_pin
ctrb_ := $18000000 + pos_pin & $3F
if neg_pin > -1
dira_[neg_pin >> 5 & 1] |= |< neg_pin
ctrb_ += $04000000 + (neg_pin & $3F) << 9
'If fm offset is valid, ready ctra for pll mode with divide-by-16 (else disabled)
if fm_offset > -1
ctra_ := $05800000
'Ready frqa value for fm offset
repeat 33
frqa_ <<= 1
if fm_offset => clkfreq
fm_offset -= clkfreq
frqa_++
fm_offset <<= 1
'Ready 20KHz sample period
cnt_ := clkfreq / 20_000
'Launch vocal tract cog
return cog := cognew(@entry, @attenuation) + 1
PUB stop
'' Stop vocal tract driver - frees a cog
'If already running, stop vocal tract cog
if cog
cogstop(cog~ - 1)
'Reset variables and buffers
longfill(@index, 0, constant(3 + 6 + frame_buffer_longs))
PUB set_attenuation(level)
'' Set master attenuation level (0..7, initially 0)
attenuation := level
PUB set_pace(percentage)
'' Set pace to some percentage (initially 100)
pace := percentage
PUB go(time)
'' Queue current parameters to transition over time
''
'' actual time = integer(time * 100 / pace) #> 2 * 700µs (at least 1400µs, see set_pace)
'Wait until frame available (first long will be zeroed)
repeat while frames[index]
'Load parameters into frame
bytemove(@frames[index] + 3, tract, 13)
'Write stepsize into frame (non-0 alerts vocal tract that frame is ready)
frames[index] |= $01000000 / (time * 100 / pace #> 2)
'Increment frame index
index := (index + frame_longs) & constant(frame_buffer_longs - 1)
PUB full : status
'' Returns true if the parameter queue is full
'' (useful for checking if "go" would have to wait)
return frames[index]
PUB empty : status | i
'' Returns true if the parameter queue is empty
'' (useful for detecting when the vocal tract is finished)
repeat i from 0 to constant(frame_buffers - 1)
if frames[i * frame_longs]
return {false}
return true
PUB sample_ptr : ptr
'' Returns the address of the long which receives the audio samples in real-time
'' (signed 32-bit values updated at 20KHz)
return @sample
PUB aural_id : id
'' Returns the id of the cog executing the vocal tract algorithm
'' (for connecting a broadcast tv driver with the aural subcarrier)
return cog - 1
DAT
' ┌──────────────────┐
' │ Initialization │
' └──────────────────┘
entry org
:zero mov reserves,#0 'zero all reserved data
add :zero,d0
djnz clear_cnt,#:zero
mov t1,#2*15 'assemble 15 multiply steps into reserves
:minst mov mult_steps,mult_step '(saves hub memory)
add :minst,d0s0
test t1,#1 wc
if_c sub :minst,#2
djnz t1,#:minst
mov mult_ret,antilog_ret 'write 'ret' after last instruction
mov t1,#13 'assemble 13 cordic steps into reserves
:cstep mov t2,#8 '(saves hub memory)
:cinst mov cordic_steps,cordic_step
add :cinst,d0s0
djnz t2,#:cinst
sub :cinst,#8
add cordic_dx,#1
add cordic_dy,#1
add cordic_a,#1
djnz t1,#:cstep
mov cordic_ret,antilog_ret 'write 'ret' over last instruction
mov t1,par 'get dira/dirb/ctra/ctrb
add t1,#2*4
mov t2,#4
:regs rdlong dira,t1
add t1,#4
add :regs,d0
djnz t2,#:regs
rdlong frqa_center,t1 'get frqa center
add t1,#4 'get cnt ticks
rdlong cnt_ticks,t1
mov cnt_value,cnt 'prepare for initial waitcnt
add cnt_value,cnt_ticks
' ┌────────────────────┐
' │ Vocal Tract Loop │
' └────────────────────┘
' Wait for next sample period, then output sample
loop waitcnt cnt_value,cnt_ticks 'wait for sample period
rdlong t1,par 'perform master attenuation
sar x,t1
mov t1,x 'update fm aural subcarrier for tv broadcast
sar t1,#10
add t1,frqa_center
mov frqa,t1
mov t1,x 'update duty cycle output for pin driving
add t1,h80000000
mov frqb,t1
mov t1,par 'update sample receiver in main memory
add t1,#1*4
wrlong x,t1
' White noise source
test lfsr,lfsr_taps wc 'iterate lfsr three times
rcl lfsr,#1
test lfsr,lfsr_taps wc
rcl lfsr,#1
test lfsr,lfsr_taps wc
rcl lfsr,#1
' Aspiration
mov t1,aa 'aspiration amplitude
mov t2,lfsr
call #mult
sar t1,#8 'set x
mov x,t1
' Vibrato
mov t1,vr 'vibrato rate
shr t1,#10
add vphase,t1
mov t1,vp 'vibrato pitch
mov t2,vphase
call #sine
add t1,gp 'sum glottal pitch (+) into vibrato pitch (+/-)
' Glottal pulse
shr t1,#2 'divide final pitch by 3 to mesh with
mov t2,t1 '...12 notes/octave musical scale
shr t2,#2 '(multiply by %0.0101010101010101)
add t1,t2
mov t2,t1
shr t2,#4
add t1,t2
mov t2,t1
shr t2,#8
add t1,t2
add t1,tune 'tune scale so that gp=100 produces 110.00Hz (A2)
call #antilog 'convert pitch (log frequency) to phase delta
add gphase,t2
mov t1,gphase 'convert phase to glottal pulse sample
call #antilog
sub t2,h40000000
mov t1,ga
call #sine
sar t1,#6 'add to x
add x,t1
' Vocal tract formants
mov y,#0 'reset y
mov a,f1 'formant1, sum and rotate (x,y)
add x,f1x
add y,f1y
call #cordic
mov f1x,x
mov f1y,y
mov a,f2 'formant2, sum and rotate (x,y)
add x,f2x
add y,f2y
call #cordic
mov f2x,x
mov f2y,y
mov a,f3 'formant3, sum and rotate (x,y)
add x,f3x
add y,f3y
call #cordic
mov f3x,x
mov f3y,y
mov a,f4 'formant4, sum and rotate (x,y)
add x,f4x
add y,f4y
call #cordic
mov f4x,x
mov f4y,y
' Nasal anti-formant
add nx,x 'subtract from x (nx negated)
mov a,nf 'nasal frequency
call #cordic
mov t1,na 'nasal amplitude
mov t2,x
call #mult
mov x,nx 'restore x
neg nx,t1 'negate nx
' Frication
mov t1,lfsr 'phase noise
sar t1,#3
add fphase,t1
sar t1,#1
add fphase,t1
mov t1,ff 'frication frequency
shr t1,#1
add fphase,t1
mov t1,fa 'frication amplitude
mov t2,fphase
call #sine
add x,t1 'add to x
' Handle frame
jmp :ret 'run segment of frame handler, return to loop
' ┌─────────────────┐
' │ Frame Handler │
' └─────────────────┘
:ret long :wait 'pointer to next frame handler routine
:wait jmpret :ret,#loop '(6 or 17.5 cycles)
mov frame_ptr,par 'check for next frame
add frame_ptr,#8*4 'point past miscellaneous data
add frame_ptr,frame_index 'point to start of frame
rdlong step_size,frame_ptr 'get stepsize
and step_size,h00FFFFFF wz 'isolate stepsize and check if not 0
if_nz jmp #:next 'if not 0, next frame ready
mov :final1,:finali 'no frame ready, ready to finalize parameters
mov frame_cnt,#13 'iterate aa..ff
:final jmpret :ret,#loop '(13.5 or 4 cycles)
:final1 mov par_curr,par_next 'current parameter = next parameter
add :final1,d0s0 'update pointers
djnz frame_cnt,#:final 'another parameter?
jmp #:wait 'check for next frame
:next add step_size,#1 'next frame ready, insure accurate accumulation
mov step_acc,step_size 'initialize step accumulator
movs :set1,#par_next 'ready to get parameters and steps for aa..ff
movd :set2,#par_curr
movd :set3,#par_next
movd :set4,#par_step
add frame_ptr,#3 'point to first parameter
mov frame_cnt,#13 'iterate aa..ff
:set jmpret :ret,#loop '(19.5 or 46.5 cycles)
rdbyte t1,frame_ptr 'get new parameter
shl t1,#24 'msb justify
:set1 mov t2,par_next 'get next parameter
:set2 mov par_curr,t2 'current parameter = next parameter
:set3 mov par_next,t1 'next parameter = new parameter
sub t1,t2 wc 'get next-current delta with sign in c
negc t1,t1 'make delta absolute (by c, not msb)
rcl vscl,#1 wz, nr 'save sign into nz (vscl unaffected)
mov t2,#8 'multiply delta by step size
:mult shl t1,#1 wc
if_c add t1,step_size
djnz t2,#:mult
:set4 negnz par_step,t1 'set signed step
add :set1,#1 'update pointers for next parameter+step
add :set2,d0
add :set3,d0
add :set4,d0
add frame_ptr,#1
djnz frame_cnt,#:set 'another parameter?
:stepframe jmpret :ret,#loop '(47.5 or 8 cycles)
mov :step1,:stepi 'ready to step parameters
mov frame_cnt,#13 'iterate aa..ff
:step jmpret :ret,#loop '(3 or 4 cycles)
:step1 add par_curr,par_step 'step parameter
add :step1,d0s0 'update pointers for next parameter+step
djnz frame_cnt,#:step 'another parameter?
add step_acc,step_size 'accumulate frame steps
test step_acc,h01000000 wc 'check for frame steps done
if_nc jmp #:stepframe 'another frame step?
sub frame_ptr,#frame_bytes 'zero stepsize in frame to signal frame done
wrlong vscl,frame_ptr
add frame_index,#frame_bytes'point to next frame
and frame_index,#frame_buffer_bytes - 1
jmp #:wait 'check for next frame
:finali mov par_curr,par_next 'instruction used to finalize parameters
:stepi add par_curr,par_step 'instruction used to step parameters
' ┌────────────────────┐
' │ Math Subroutines │
' └────────────────────┘
' Antilog
'
' in: t1 = log (top 4 bits = whole number, next 11 bits = fraction)
'
' out: t2 = antilog ($00010000..$FFEA0000)
antilog mov t2,t1
shr t2,#16 'position 11-bit fraction
shr t1,#16+12 'position 4-bit whole number
and t2,h00000FFE 'get table offset
or t2,h0000D000 'get table base
rdword t2,t2 'lookup fractional antilog
or t2,h00010000 'insert leading bit
shl t2,t1 'shift up by whole number
antilog_ret ret
' Scaled sine
'
' in: t1 = unsigned scale (15 top bits used)
' t2 = angle (13 top bits used)
'
' out: t1 = 17-bit * 15-bit scaled sine ($80014000..$7FFEC000)
sine shr t2,#32-13 'get 13-bit angle
test t2,h00001000 wz 'get sine quadrant 3|4 into nz
test t2,h00000800 wc 'get sine quadrant 2|4 into c
negc t2,t2 'if sine quadrant 2|4, negate table offset
or t2,h00007000 'insert sine table base address >> 1
shl t2,#1 'shift left to get final word address
rdword t2,t2 'read sine word from table
negnz t2,t2 'if quadrant 3|4, negate word
shl t2,#15 'msb-justify result
'multiply follows...
' Multiply
'
' in: t1 = unsigned multiplier (15 top bits used)
' t2 = signed multiplicand (17 top bits used)
'
' out: t1 = 32-bit signed product
mult shr t1,#32-15 'position unsigned multiplier
sar t2,#15 'position signed multiplicand
shl t2,#15-1
jmp #mult_steps 'do multiply steps
mult_step sar t1,#1 wc 'multiply step that gets assembled into reserves (x15)
if_c add t1,t2
' Cordic rotation
'
' in: a = 0 to <90 degree angle (~13 top bits used)
' x,y = signed coordinates
'
' out: x,y = scaled and rotated signed coordinates
cordic sar x,#1 'multiply (x,y) by %0.10011001 (0.60725 * 0.984)
mov t1,x '...for cordic pre-scaling and slight damping
sar t1,#3
add x,t1
mov t1,x
sar t1,#4
add x,t1
sar y,#1
mov t1,y
sar t1,#3
add y,t1
mov t1,y
sar t1,#4
add y,t1
mov t1,x 'do first cordic step
sub x,y
add y,t1
sub a,h80000000 wc
jmp #cordic_steps+1 'do subsequent cordic steps (skip first instruction)
cordic_step mov a,a wc 'cordic step that gets assembled into reserves (x13)
mov t1,y
cordic_dx sar t1,#1 '(source incremented for each step)
mov t2,x
cordic_dy sar t2,#1 '(source incremented for each step)
sumnc x,t1
sumc y,t2
cordic_a sumnc a,cordic_delta '(source incremented for each step)
' ┌────────────────┐
' │ Defined Data │
' └────────────────┘
tune long $66920000 'scale tuned to 110.00Hz at gp=100 (manually calibrated)
lfsr long 1 'linear feedback shift register for noise generation
lfsr_taps long $80061000
cordic_delta long $4B901476 'cordic angle deltas (first is h80000000)
long $27ECE16D
long $14444750
long $0A2C350C
long $05175F85
long $028BD879
long $0145F154
long $00A2F94D
long $00517CBB
long $0028BE60
long $00145F30
long $000A2F98
h80000000 long $80000000 'miscellaneous constants greater than 9 bits
h40000000 long $40000000
h01000000 long $01000000
h00FFFFFF long $00FFFFFF
h00010000 long $00010000
h0000D000 long $0000D000
h00007000 long $00007000
h00001000 long $00001000
h00000FFE long $00000FFE
h00000800 long $00000800
d0 long $00000200 'destination/source field increments
d0s0 long $00000201
clear_cnt long $1F0 - reserves 'number of reserved registers to clear on startup
' ┌──────────────────────────────────────────────────┐
' │ Undefined Data (zeroed by initialization code) │
' └──────────────────────────────────────────────────┘
reserves
frqa_center res 1 'reserved registers that get cleared on startup
cnt_ticks res 1
cnt_value res 1
frame_index res 1
frame_ptr res 1
frame_cnt res 1
step_size res 1
step_acc res 1
vphase res 1
gphase res 1
fphase res 1
f1x res 1
f1y res 1
f2x res 1
f2y res 1
f3x res 1
f3y res 1
f4x res 1
f4y res 1
nx res 1
a res 1
x res 1
y res 1
t1 res 1
t2 res 1
par_curr '*** current parameters
aa res 1 'aspiration amplitude
ga res 1 'glottal amplitude
gp res 1 'glottal pitch
vp res 1 'vibrato pitch
vr res 1 'vibrato rate
f1 res 1 'formant1 frequency
f2 res 1 'formant2 frequency
f3 res 1 'formant3 frequency
f4 res 1 'formant4 frequency
na res 1 'nasal amplitude
nf res 1 'nasal frequency
fa res 1 'frication amplitude
ff res 1 'frication frequency
par_next res 13 '*** next parameters
par_step res 13 '*** parameter steps
mult_steps res 2 * 15 'assembly area for multiply steps w/ret
mult_ret
sine_ret res 1
cordic_steps res 8 * 13 - 1 'assembly area for cordic steps w/ret
cordic_ret res 1
{{
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 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. │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
}}