Re: very simple phase-locked loop pitch detection in C
Dave Long <[email protected]>
| Newsgroups | gmane.culture.people.kragen.discuss |
|---|---|
| Message-ID | <[email protected]> |
> /* A PLL in one line of C. arecord | ./tinypll | aplay */
> main(a,b){for(;;)putchar(b+=16+(a+=(b&256?1:-1)*getchar()-a/
> 512)/1024);}
>
> However, it took me most of two days to write that one line of
> text, and I
> still don't fully understand it.
after writing some python (vide infra), here's my interpretation of
that line:
b is the local oscillator, which produces a triangle wave ramping
between (0-255)
a is the feedback control
the +(b&256?1:-1) term chops the input[0,1] from getchar(), but at
only half of b's frequency (hence the doubled output)
the -a/512 term produces a "leaky integrator" to low-pass the feedback
-Dave
[0] this is a Horowitz & Hill type I (dot product) phase comparison;
the python below uses an edge-based lead-lag[2] (type II) phase
comparison. H&H think that the problem most people have with PLL's
is attempting to "cut & try" as we have both done, instead of
calculating the feedback loop properly; they point out that this is
somewhat trickier than usual because one measures phase but controls
frequency, hence the feedback loop is already lagging by at least a
quarter-wave.
[1] cf lock-in amplification, a related hack. Could dreaming be an
epiphenomenon of an evolved "lock-in amplification" for learning (in
which the brain's inputs are chopped between "reality" while awake
and "dreams" while asleep) in order to cope with the fact that the
SNR of reality often leaves much to be desired?
[2] presumably one could make simple adjustments to "zeroph" to deal
with noisier (read "real") input. cf o-scope trigger parameters
:: :: ::
import math
# wav: produce an "analog" sampled waveform at a given "frequency"
# zeroph: "digitization": true at positive zero-crossings (flag cycle
starts)
cycle = lambda a,b: math.sin(a*math.pi/64*b)
wav = lambda f: [cycle(f,x) for x in range(1024)]
zeroph = lambda w: [(i<0 and j>0) for i,j in zip(w[:-1],w[1:])]
# cdelta: -ve in lead (0-1), +ve in lag (1-0), 0 when matched (0-0)
or (1-1)
# cprime: accumulate signal for each sample that lag or lead is present
# compare: Horowitz & Hill type II phase detector (sum all lags and/
or leads)
cdelta = lambda (i,j): j-i
cprime = lambda ds,ll: ds+[ds[-1] + cdelta(ll)]
compare = lambda ref,sig: sum(reduce(cprime,zip(zeroph(ref),zeroph
(sig)),[0]))
# odelta: calculate the adjustment by comparing lead/lag of cycle starts
# oprime: scan-update the local oscillator value
odelta = lambda osc, sig: compare(wav(osc),wav(sig))/4096.0
oprime = lambda os, sig: os+[os[-1] + odelta(os[-1],sig)]
# sig: the signal to track, varying in "frequency" between 2 and 22
# osc: the local oscillator, initial "frequency" is 100
sig = [12 - 10*cycle(4,r) for r in range(40)]
osc = reduce(oprime, sig, [100])[1:]
table = lambda ls: "\n".join(ls)
print table(["%s\t%s\t%s" % ("signal freq","osc freq","ratio")] +
["%f\t%f\t%f" % (s,o,o/s) for o,s in zip(osc,sig)])
--
To unsubscribe: http://lists.canonical.org/mailman/listinfo/kragen-discuss