c26b6aac82
El SDK CI - dev / build-and-test (pull_request) Failing after 4m18s
The speaker and the voice-fetch landed in the previous commit. This is the remainder of the 939-line Swift program, ported, and the line it draws is between DEVICE and ARITHMETIC rather than between languages. Two things stay realizers, because they are the two things El cannot express as arithmetic: handing a buffer to the DAC and waiting for it to drain (el_audio_darwin.m), and asking the OS for samples off a mic or frames off a camera (el_capture_darwin.m). Both are their own translation units declared in el_runtime.h, never patches to el_runtime.c. Everything else is El. WAV decode, LPC autocorrelation, Levinson-Durbin at order 16, formant extraction off the all-pole envelope, source-filter resynthesis, and the three descriptors are organ_dsp.el. Consent, disclosure and the scene descriptor are organ.el. Barge-in, yield-or-hold, backchannel and resume are organ_converse.el. The organ never learns a word. Codes and phoneme geometry arrive from the language side; the organ turns them into samples and gets the samples out the speaker, and runs the same trip in reverse for the senses. No lexicon, no grapheme-to-phoneme, by design. Barge-in needed pause/resume and a real DAC position rather than a tick counter, because "finish the buffer" is not barge-in and a queue holding three buffers is a third of a second wrong about where it is. An injected barge also had to fire once rather than stay true, which is otherwise a livelock the moment a backchannel resumes. Measured against the Swift on out/mic_room.wav: seconds, rms, peak, zcr, centroid and F0 agree to every printed digit; formants F1-F5 and bandwidths B1-B5 are identical. imitate cannot match bit-for-bit because the Swift excites unvoiced frames with Double.random — two Swift runs correlate 0.957 with each other and El correlates 0.958 with Swift, so the port is as close to the original as the original is to itself. Verified end to end: consent fails closed on both locks, real mic capture (16000 frames), real camera frame (1920x1080 -> 15 numbers), voiceprint, imitate, hear-imitate, a voice learned by ear and fetched back out of the engram, and all five converse paths with real audio. The binary contains zero afplay/Swift strings and spawns no child process while speaking.
1020 lines
32 KiB
EmacsLisp
1020 lines
32 KiB
EmacsLisp
// organ_dsp.el — the AFFERENT DSP organ, own-core, ported from peripheral/src/periph.swift.
|
|
//
|
|
// The mirror of speech.el: where speech.el RENDERS meaning out through a voice
|
|
// (efferent), this module HEARS — it takes a raw 16-bit PCM RIFF/WAVE capture
|
|
// and metabolizes it into GEOMETRY: a compact 8-number audio descriptor, a
|
|
// voice-signature (F0 + formants F1-F5 by LPC), and an LPC analysis-resynthesis
|
|
// that speaks the heard voice back from its own signature.
|
|
//
|
|
// Everything here is float physics (math_sin/math_cos/math_sqrt), not the
|
|
// fixed-point integer path speech.el uses for synthesis — the analysis side
|
|
// needs the dynamic range that autocorrelation and Levinson-Durbin demand.
|
|
//
|
|
// Binary I/O note: an El String truncates at the first NUL, so a WAV can never
|
|
// be read through fs_read(). We read it through fs_read_b64_chunk(), which
|
|
// hands back plain-ASCII base64 of a byte window, and decode that base64 HERE,
|
|
// in El, into a [Int] of byte values. Chunks are a multiple of 3 bytes so each
|
|
// base64 window decodes cleanly with no padding except the final one.
|
|
//
|
|
// Every function is prefixed dsp_ so nothing here can collide with speech.el
|
|
// (write_wav / wav_le16 / wav_le32 / sp_* / voice_* all live there and are NOT
|
|
// redefined). imitate returns [Int] samples — hand them to speech.el's write_wav.
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Base64 -> bytes (own-core; the only way binary reaches El intact)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Standard RFC 4648 alphabet A-Za-z0-9+/ -> 0..63. Padding '=' and any other
|
|
// character -> -1 (a sentinel; there is no exception handling in El).
|
|
fn dsp_b64_val(c: Int) -> Int {
|
|
if c >= 65 {
|
|
if c <= 90 {
|
|
return c - 65
|
|
}
|
|
}
|
|
if c >= 97 {
|
|
if c <= 122 {
|
|
return c - 71
|
|
}
|
|
}
|
|
if c >= 48 {
|
|
if c <= 57 {
|
|
return c + 4
|
|
}
|
|
}
|
|
if c == 43 {
|
|
return 62
|
|
}
|
|
if c == 47 {
|
|
return 63
|
|
}
|
|
return 0 - 1
|
|
}
|
|
|
|
// Read a whole file as a list of byte values 0..255. Empty list on failure.
|
|
fn dsp_read_bytes(path: String) -> [Int] {
|
|
let bytes: [Int] = native_list_empty()
|
|
let size: Int = fs_size(path)
|
|
if size <= 0 {
|
|
return bytes
|
|
}
|
|
let chunk: Int = 60000 // multiple of 3 -> no interior padding
|
|
let off: Int = 0
|
|
while off < size {
|
|
let s: String = fs_read_b64_chunk(path, off, chunk)
|
|
let sl: Int = str_len(s)
|
|
if sl < 4 {
|
|
return bytes
|
|
}
|
|
let i: Int = 0
|
|
while i + 3 < sl {
|
|
let c0: Int = dsp_b64_val(str_char_code(s, i))
|
|
let c1: Int = dsp_b64_val(str_char_code(s, i + 1))
|
|
let c2: Int = dsp_b64_val(str_char_code(s, i + 2))
|
|
let c3: Int = dsp_b64_val(str_char_code(s, i + 3))
|
|
if c0 < 0 {
|
|
return bytes
|
|
}
|
|
if c1 < 0 {
|
|
return bytes
|
|
}
|
|
let b0: Int = c0 * 4 + c1 / 16
|
|
bytes = native_list_append(bytes, b0)
|
|
if c2 >= 0 {
|
|
let lo1: Int = c1 - (c1 / 16) * 16
|
|
let b1: Int = lo1 * 16 + c2 / 4
|
|
bytes = native_list_append(bytes, b1)
|
|
if c3 >= 0 {
|
|
let lo2: Int = c2 - (c2 / 4) * 4
|
|
let b2: Int = lo2 * 64 + c3
|
|
bytes = native_list_append(bytes, b2)
|
|
}
|
|
}
|
|
i = i + 4
|
|
}
|
|
off = off + chunk
|
|
}
|
|
return bytes
|
|
}
|
|
|
|
fn dsp_rd16(b: [Int], o: Int) -> Int {
|
|
let b0: Int = native_list_get(b, o)
|
|
let b1: Int = native_list_get(b, o + 1)
|
|
return b0 + b1 * 256
|
|
}
|
|
|
|
fn dsp_rd32(b: [Int], o: Int) -> Int {
|
|
let b0: Int = native_list_get(b, o)
|
|
let b1: Int = native_list_get(b, o + 1)
|
|
let b2: Int = native_list_get(b, o + 2)
|
|
let b3: Int = native_list_get(b, o + 3)
|
|
return b0 + b1 * 256 + b2 * 65536 + b3 * 16777216
|
|
}
|
|
|
|
// Four bytes at o compared against a 4-char ASCII chunk id.
|
|
fn dsp_chunk_is(b: [Int], o: Int, id: String) -> Bool {
|
|
let k: Int = 0
|
|
while k < 4 {
|
|
let got: Int = native_list_get(b, o + k)
|
|
let want: Int = str_char_code(id, k)
|
|
if got != want {
|
|
return false
|
|
}
|
|
k = k + 1
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// readWavSamples — 16-bit PCM RIFF/WAVE -> normalized samples.
|
|
// Walks chunks to find 'fmt ' and 'data', so JUNK/FLLR padding (which
|
|
// AVAudioRecorder emits) is stepped over rather than mistaken for audio.
|
|
// Channel 0 only if stereo.
|
|
//
|
|
// The returned list is PACKED: [ sr, ch, n, s0, s1, ... s(n-1) ] with the three
|
|
// header numbers carried as Floats (El has no tuples). Use dsp_wav_sr /
|
|
// dsp_wav_ch / dsp_wav_n / dsp_wav_pcm to open it. Empty list on failure.
|
|
// ---------------------------------------------------------------------------
|
|
fn dsp_read_wav(path: String) -> [Float] {
|
|
let out: [Float] = native_list_empty()
|
|
let d: [Int] = dsp_read_bytes(path)
|
|
let dn: Int = native_list_len(d)
|
|
if dn <= 44 {
|
|
return out
|
|
}
|
|
let ch: Int = 0
|
|
let sr: Int = 0
|
|
let bits: Int = 0
|
|
let o: Int = 12
|
|
while o + 8 <= dn {
|
|
let sz: Int = dsp_rd32(d, o + 4)
|
|
if dsp_chunk_is(d, o, "fmt ") {
|
|
if o + 24 <= dn {
|
|
ch = dsp_rd16(d, o + 10)
|
|
sr = dsp_rd32(d, o + 12)
|
|
bits = dsp_rd16(d, o + 22)
|
|
}
|
|
}
|
|
if dsp_chunk_is(d, o, "data") {
|
|
if bits != 16 {
|
|
return out
|
|
}
|
|
if ch <= 0 {
|
|
return out
|
|
}
|
|
// Faithful to periph.swift, including its `d.count - 1` bound and
|
|
// the `while i + 1 < end` test — the last sample of a file whose
|
|
// data chunk runs to EOF is dropped there, so it is dropped here.
|
|
let start: Int = o + 8
|
|
let end: Int = start + sz
|
|
if end > dn - 1 {
|
|
end = dn - 1
|
|
}
|
|
let samples: [Float] = native_list_empty()
|
|
let step: Int = 2 * ch
|
|
let i: Int = start
|
|
let count: Int = 0
|
|
while i + 1 < end {
|
|
let v: Int = dsp_rd16(d, i)
|
|
if v >= 32768 {
|
|
v = v - 65536
|
|
}
|
|
let f: Float = int_to_float(v) / 32768.0
|
|
samples = native_list_append(samples, f)
|
|
count = count + 1
|
|
i = i + step
|
|
}
|
|
let srf: Float = int_to_float(sr)
|
|
let chf: Float = int_to_float(ch)
|
|
let nf: Float = int_to_float(count)
|
|
out = native_list_append(out, srf)
|
|
out = native_list_append(out, chf)
|
|
out = native_list_append(out, nf)
|
|
let j: Int = 0
|
|
while j < count {
|
|
let sv: Float = native_list_get(samples, j)
|
|
out = native_list_append(out, sv)
|
|
j = j + 1
|
|
}
|
|
return out
|
|
}
|
|
let adv: Int = 8 + sz + (sz - (sz / 2) * 2)
|
|
if adv <= 0 {
|
|
return out
|
|
}
|
|
o = o + adv
|
|
}
|
|
return out
|
|
}
|
|
|
|
fn dsp_wav_sr(w: [Float]) -> Int {
|
|
if native_list_len(w) < 3 {
|
|
return 0
|
|
}
|
|
let v: Float = native_list_get(w, 0)
|
|
return float_to_int(v)
|
|
}
|
|
|
|
fn dsp_wav_ch(w: [Float]) -> Int {
|
|
if native_list_len(w) < 3 {
|
|
return 0
|
|
}
|
|
let v: Float = native_list_get(w, 1)
|
|
return float_to_int(v)
|
|
}
|
|
|
|
fn dsp_wav_n(w: [Float]) -> Int {
|
|
if native_list_len(w) < 3 {
|
|
return 0
|
|
}
|
|
let v: Float = native_list_get(w, 2)
|
|
return float_to_int(v)
|
|
}
|
|
|
|
// The bare sample list, unpacked from the header-prefixed form.
|
|
fn dsp_wav_pcm(w: [Float]) -> [Float] {
|
|
let out: [Float] = native_list_empty()
|
|
let n: Int = dsp_wav_n(w)
|
|
let i: Int = 0
|
|
while i < n {
|
|
let v: Float = native_list_get(w, i + 3)
|
|
out = native_list_append(out, v)
|
|
i = i + 1
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// small float helpers (El has no unary minus on Float in every position, and
|
|
// no min/max builtin — so they are written out)
|
|
// ---------------------------------------------------------------------------
|
|
fn dsp_fabs(v: Float) -> Float {
|
|
if v < 0.0 {
|
|
return 0.0 - v
|
|
}
|
|
return v
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// computeAudio — the compact audio descriptor.
|
|
// Returns the 8-number vector [seconds, sr, ch, rms, peak, zcr, centroid, f0].
|
|
// Empty list if the WAV cannot be read (no exceptions in El — sentinels only).
|
|
// ---------------------------------------------------------------------------
|
|
fn dsp_compute_audio(path: String) -> [Float] {
|
|
let vec: [Float] = native_list_empty()
|
|
let w: [Float] = dsp_read_wav(path)
|
|
let n: Int = dsp_wav_n(w)
|
|
if n <= 0 {
|
|
return vec
|
|
}
|
|
let sr: Int = dsp_wav_sr(w)
|
|
let ch: Int = dsp_wav_ch(w)
|
|
let s: [Float] = dsp_wav_pcm(w)
|
|
let nf: Float = int_to_float(n)
|
|
let srf: Float = int_to_float(sr)
|
|
let seconds: Float = nf / srf
|
|
|
|
// energy / peak / zero crossings
|
|
let sumsq: Float = 0.0
|
|
let peak: Float = 0.0
|
|
let zc: Float = 0.0
|
|
let i: Int = 0
|
|
while i < n {
|
|
let v: Float = native_list_get(s, i)
|
|
sumsq = sumsq + v * v
|
|
let av: Float = dsp_fabs(v)
|
|
if av > peak {
|
|
peak = av
|
|
}
|
|
if i > 0 {
|
|
let pv: Float = native_list_get(s, i - 1)
|
|
let a: Bool = pv < 0.0
|
|
let b: Bool = v < 0.0
|
|
if a != b {
|
|
zc = zc + 1.0
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
let rms: Float = math_sqrt(sumsq / nf)
|
|
let zcr: Float = zc / nf * srf // ~2*dominant freq for tonal
|
|
|
|
// Spectral centroid via a coarse 64-bin DFT on a centered 2048 window.
|
|
let ww: Int = 2048
|
|
if n < ww {
|
|
ww = n
|
|
}
|
|
let off: Int = (n - ww) / 2
|
|
if off < 0 {
|
|
off = 0
|
|
}
|
|
let two: Float = 2.0
|
|
let pi: Float = math_pi()
|
|
let num: Float = 0.0
|
|
let den: Float = 0.0
|
|
let bins: Int = 64
|
|
let binsf: Float = 128.0 // Double(2*bins)
|
|
let k: Int = 1
|
|
while k < bins {
|
|
let kf: Float = int_to_float(k)
|
|
let f: Float = kf * srf / binsf
|
|
let re: Float = 0.0
|
|
let im: Float = 0.0
|
|
let j: Int = 0
|
|
while j < ww {
|
|
let jf: Float = int_to_float(j)
|
|
let ang: Float = (0.0 - two) * pi * kf * jf / binsf
|
|
let xv: Float = native_list_get(s, off + j)
|
|
let cv: Float = math_cos(ang)
|
|
let sv: Float = math_sin(ang)
|
|
re = re + xv * cv
|
|
im = im + xv * sv
|
|
j = j + 1
|
|
}
|
|
let mag: Float = math_sqrt(re * re + im * im)
|
|
num = num + f * mag
|
|
den = den + mag
|
|
k = k + 1
|
|
}
|
|
let centroid: Float = 0.0
|
|
if den > 0.0 {
|
|
centroid = num / den
|
|
}
|
|
|
|
// F0 by autocorrelation over the plausible speech range 70-400 Hz.
|
|
let lag_min: Int = sr / 400
|
|
let lag_max: Int = sr / 70
|
|
if lag_max > n - 1 {
|
|
lag_max = n - 1
|
|
}
|
|
let bound: Int = off + ww
|
|
if bound > n {
|
|
bound = n
|
|
}
|
|
let best_lag: Int = 0
|
|
let best_corr: Float = 0.0
|
|
if lag_max > lag_min {
|
|
let lag: Int = lag_min
|
|
while lag <= lag_max {
|
|
let c: Float = 0.0
|
|
let p: Int = 0
|
|
while p + lag < bound {
|
|
let a1: Float = native_list_get(s, off + p)
|
|
let a2: Float = native_list_get(s, off + p + lag)
|
|
c = c + a1 * a2
|
|
p = p + 1
|
|
}
|
|
if c > best_corr {
|
|
best_corr = c
|
|
best_lag = lag
|
|
}
|
|
lag = lag + 1
|
|
}
|
|
}
|
|
let f0: Float = 0.0
|
|
if best_lag > 0 {
|
|
f0 = srf / int_to_float(best_lag)
|
|
}
|
|
|
|
vec = native_list_append(vec, seconds)
|
|
vec = native_list_append(vec, srf)
|
|
let chf: Float = int_to_float(ch)
|
|
vec = native_list_append(vec, chf)
|
|
vec = native_list_append(vec, rms)
|
|
vec = native_list_append(vec, peak)
|
|
vec = native_list_append(vec, zcr)
|
|
vec = native_list_append(vec, centroid)
|
|
vec = native_list_append(vec, f0)
|
|
return vec
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// LPC core: hamming, autocorr, Levinson-Durbin, formant peak-pick, pitch.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn dsp_hamming(x: [Float]) -> [Float] {
|
|
let n: Int = native_list_len(x)
|
|
if n < 2 {
|
|
return x
|
|
}
|
|
let out: [Float] = native_list_empty()
|
|
let pi: Float = math_pi()
|
|
let dn: Float = int_to_float(n - 1)
|
|
let i: Int = 0
|
|
while i < n {
|
|
let v: Float = native_list_get(x, i)
|
|
let ang: Float = 2.0 * pi * int_to_float(i) / dn
|
|
let cv: Float = math_cos(ang)
|
|
let wv: Float = 0.54 - 0.46 * cv
|
|
out = native_list_append(out, v * wv)
|
|
i = i + 1
|
|
}
|
|
return out
|
|
}
|
|
|
|
// r[lag] = sum_i x[i]*x[i-lag], lag = 0..p. Returns p+1 numbers.
|
|
fn dsp_autocorr(x: [Float], p: Int) -> [Float] {
|
|
let n: Int = native_list_len(x)
|
|
let r: [Float] = native_list_empty()
|
|
let lag: Int = 0
|
|
while lag <= p {
|
|
let acc: Float = 0.0
|
|
let i: Int = lag
|
|
while i < n {
|
|
let a: Float = native_list_get(x, i)
|
|
let b: Float = native_list_get(x, i - lag)
|
|
acc = acc + a * b
|
|
i = i + 1
|
|
}
|
|
r = native_list_append(r, acc)
|
|
lag = lag + 1
|
|
}
|
|
return r
|
|
}
|
|
|
|
// Levinson-Durbin -> LPC coeffs a[0..p] with A(z) = 1 + sum a[k] z^-k.
|
|
// Returns p+2 numbers: a[0..p] followed by the residual energy at index p+1.
|
|
fn dsp_levinson(r: [Float], p: Int) -> [Float] {
|
|
let a: [Float] = native_list_empty()
|
|
a = native_list_append(a, 1.0)
|
|
let z: Int = 1
|
|
while z <= p {
|
|
a = native_list_append(a, 0.0)
|
|
z = z + 1
|
|
}
|
|
let err: Float = native_list_get(r, 0)
|
|
if err <= 0.0 {
|
|
a = native_list_append(a, 0.0)
|
|
return a
|
|
}
|
|
let i: Int = 1
|
|
let stopped: Bool = false
|
|
while i <= p {
|
|
if stopped {
|
|
i = p + 1
|
|
} else {
|
|
let acc: Float = native_list_get(r, i)
|
|
if i > 1 {
|
|
let j: Int = 1
|
|
while j < i {
|
|
let aj: Float = native_list_get(a, j)
|
|
let rij: Float = native_list_get(r, i - j)
|
|
acc = acc + aj * rij
|
|
j = j + 1
|
|
}
|
|
}
|
|
let k: Float = (0.0 - acc) / err
|
|
// na = a, then na[i] = k, then na[j] = a[j] + k*a[i-j] for 1<=j<i
|
|
let na: [Float] = native_list_empty()
|
|
na = native_list_append(na, 1.0)
|
|
let m: Int = 1
|
|
while m <= p {
|
|
let v: Float = native_list_get(a, m)
|
|
if m == i {
|
|
v = k
|
|
}
|
|
if m < i {
|
|
let am: Float = native_list_get(a, m)
|
|
let aim: Float = native_list_get(a, i - m)
|
|
v = am + k * aim
|
|
}
|
|
na = native_list_append(na, v)
|
|
m = m + 1
|
|
}
|
|
a = na
|
|
err = err * (1.0 - k * k)
|
|
if err <= 0.0 {
|
|
stopped = true
|
|
}
|
|
i = i + 1
|
|
}
|
|
}
|
|
a = native_list_append(a, err)
|
|
return a
|
|
}
|
|
|
|
// Formant peaks from the LPC all-pole spectral envelope, 512 steps over 0..sr/2,
|
|
// kept in 150..5200 Hz, at most 5. Returns FLAT PAIRS [f1, bw1, f2, bw2, ...]
|
|
// with the crude -3 dB (peak/sqrt2) bandwidth estimate.
|
|
fn dsp_formants(a: [Float], sr: Int) -> [Float] {
|
|
let peaks: [Float] = native_list_empty()
|
|
let p: Int = native_list_len(a) - 1
|
|
if p < 1 {
|
|
return peaks
|
|
}
|
|
let steps: Int = 512
|
|
let stepsf: Float = 512.0
|
|
let srf: Float = int_to_float(sr)
|
|
let pi: Float = math_pi()
|
|
let mag: [Float] = native_list_empty()
|
|
let s: Int = 0
|
|
while s < steps {
|
|
let wq: Float = pi * int_to_float(s) / stepsf // 0..pi -> 0..sr/2
|
|
let re: Float = 0.0
|
|
let im: Float = 0.0
|
|
let k: Int = 0
|
|
while k <= p {
|
|
let ak: Float = native_list_get(a, k)
|
|
let ang: Float = wq * int_to_float(k)
|
|
let cv: Float = math_cos(ang)
|
|
let sv: Float = math_sin(ang)
|
|
re = re + ak * cv
|
|
im = im - ak * sv
|
|
k = k + 1
|
|
}
|
|
let d: Float = math_sqrt(re * re + im * im)
|
|
if d < 0.000000001 {
|
|
d = 0.000000001
|
|
}
|
|
mag = native_list_append(mag, 1.0 / d)
|
|
s = s + 1
|
|
}
|
|
let found: Int = 0
|
|
let t: Int = 1
|
|
while t < steps - 1 {
|
|
if found < 5 {
|
|
let m0: Float = native_list_get(mag, t - 1)
|
|
let m1: Float = native_list_get(mag, t)
|
|
let m2: Float = native_list_get(mag, t + 1)
|
|
let rise: Bool = m1 > m0
|
|
let fall: Bool = m1 >= m2
|
|
if rise {
|
|
if fall {
|
|
let f: Float = int_to_float(t) * srf / 2.0 / stepsf
|
|
if f > 150.0 {
|
|
if f < 5200.0 {
|
|
let thr: Float = m1 / 1.4142
|
|
let lo: Int = t
|
|
let scan_lo: Bool = true
|
|
while scan_lo {
|
|
if lo > 0 {
|
|
let mv: Float = native_list_get(mag, lo)
|
|
if mv > thr {
|
|
lo = lo - 1
|
|
} else {
|
|
scan_lo = false
|
|
}
|
|
} else {
|
|
scan_lo = false
|
|
}
|
|
}
|
|
let hi: Int = t
|
|
let scan_hi: Bool = true
|
|
while scan_hi {
|
|
if hi < steps - 1 {
|
|
let mv2: Float = native_list_get(mag, hi)
|
|
if mv2 > thr {
|
|
hi = hi + 1
|
|
} else {
|
|
scan_hi = false
|
|
}
|
|
} else {
|
|
scan_hi = false
|
|
}
|
|
}
|
|
let bw: Float = int_to_float(hi - lo) * srf / 2.0 / stepsf
|
|
peaks = native_list_append(peaks, f)
|
|
peaks = native_list_append(peaks, bw)
|
|
found = found + 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
t = t + 1
|
|
}
|
|
return peaks
|
|
}
|
|
|
|
// Autocorrelation pitch over 70-400 Hz with the 0.30 voicing threshold.
|
|
// Returns 0.0 for an unvoiced (or silent) frame.
|
|
fn dsp_pitch_of(frame: [Float], sr: Int) -> Float {
|
|
let n: Int = native_list_len(frame)
|
|
let lag_min: Int = sr / 400
|
|
let lag_max: Int = sr / 70
|
|
if lag_max > n - 1 {
|
|
lag_max = n - 1
|
|
}
|
|
if lag_max <= lag_min {
|
|
return 0.0
|
|
}
|
|
let r0: Float = 0.0
|
|
let i: Int = 0
|
|
while i < n {
|
|
let v: Float = native_list_get(frame, i)
|
|
r0 = r0 + v * v
|
|
i = i + 1
|
|
}
|
|
if r0 < 0.00001 {
|
|
return 0.0
|
|
}
|
|
let best_lag: Int = 0
|
|
let best: Float = 0.0
|
|
let lag: Int = lag_min
|
|
while lag <= lag_max {
|
|
let c: Float = 0.0
|
|
let j: Int = lag
|
|
while j < n {
|
|
let a: Float = native_list_get(frame, j)
|
|
let b: Float = native_list_get(frame, j - lag)
|
|
c = c + a * b
|
|
j = j + 1
|
|
}
|
|
if c > best {
|
|
best = c
|
|
best_lag = lag
|
|
}
|
|
lag = lag + 1
|
|
}
|
|
if best_lag > 0 {
|
|
let ratio: Float = best / r0
|
|
if ratio > 0.30 {
|
|
let srf: Float = int_to_float(sr)
|
|
return srf / int_to_float(best_lag)
|
|
}
|
|
}
|
|
return 0.0
|
|
}
|
|
|
|
// A copy of x[pos..pos+n) — the El stand-in for Swift's Array(x[a..<b]).
|
|
fn dsp_slice(x: [Float], pos: Int, n: Int) -> [Float] {
|
|
let out: [Float] = native_list_empty()
|
|
let total: Int = native_list_len(x)
|
|
let i: Int = 0
|
|
while i < n {
|
|
if pos + i < total {
|
|
let v: Float = native_list_get(x, pos + i)
|
|
out = native_list_append(out, v)
|
|
}
|
|
i = i + 1
|
|
}
|
|
return out
|
|
}
|
|
|
|
// median == sorted()[count/2]. There is no list-set in El, so instead of
|
|
// sorting we find the value whose rank bracket contains index count/2 —
|
|
// numerically identical to the Swift expression, without a mutable buffer.
|
|
fn dsp_median(v: [Float]) -> Float {
|
|
let n: Int = native_list_len(v)
|
|
if n == 0 {
|
|
return 0.0
|
|
}
|
|
let target: Int = n / 2
|
|
let i: Int = 0
|
|
while i < n {
|
|
let x: Float = native_list_get(v, i)
|
|
let less: Int = 0
|
|
let eq: Int = 0
|
|
let j: Int = 0
|
|
while j < n {
|
|
let y: Float = native_list_get(v, j)
|
|
if y < x {
|
|
less = less + 1
|
|
}
|
|
if y == x {
|
|
eq = eq + 1
|
|
}
|
|
j = j + 1
|
|
}
|
|
if less <= target {
|
|
if target < less + eq {
|
|
return x
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
return 0.0
|
|
}
|
|
|
|
fn dsp_min_of(v: [Float]) -> Float {
|
|
let n: Int = native_list_len(v)
|
|
if n == 0 {
|
|
return 0.0
|
|
}
|
|
let m: Float = native_list_get(v, 0)
|
|
let i: Int = 1
|
|
while i < n {
|
|
let x: Float = native_list_get(v, i)
|
|
if x < m {
|
|
m = x
|
|
}
|
|
i = i + 1
|
|
}
|
|
return m
|
|
}
|
|
|
|
fn dsp_max_of(v: [Float]) -> Float {
|
|
let n: Int = native_list_len(v)
|
|
if n == 0 {
|
|
return 0.0
|
|
}
|
|
let m: Float = native_list_get(v, 0)
|
|
let i: Int = 1
|
|
while i < n {
|
|
let x: Float = native_list_get(v, i)
|
|
if x > m {
|
|
m = x
|
|
}
|
|
i = i + 1
|
|
}
|
|
return m
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// voiceprint — the voice-signature: median F0 over voiced frames + the median
|
|
// of each formant F1..F5 (and its bandwidth), plus the F0 min/max range.
|
|
// FRAME = 400 (25 ms @ 16k), HOP = 160 (10 ms), LPC order 16.
|
|
//
|
|
// Returns PACKED: [ f0med, f0lo, f0hi, nf, f1, b1, f2, b2, ... ] where nf is
|
|
// how many formants were recovered (0..5). Empty list if unreadable.
|
|
// ---------------------------------------------------------------------------
|
|
fn dsp_voiceprint(path: String) -> [Float] {
|
|
let out: [Float] = native_list_empty()
|
|
let w: [Float] = dsp_read_wav(path)
|
|
let n: Int = dsp_wav_n(w)
|
|
let frame_len: Int = 400
|
|
let hop: Int = 160
|
|
let order: Int = 16
|
|
if n <= frame_len {
|
|
return out
|
|
}
|
|
let sr: Int = dsp_wav_sr(w)
|
|
let x: [Float] = dsp_wav_pcm(w)
|
|
|
|
let f0s: [Float] = native_list_empty()
|
|
// five formant banks + five bandwidth banks, flat lists each
|
|
let f1s: [Float] = native_list_empty()
|
|
let f2s: [Float] = native_list_empty()
|
|
let f3s: [Float] = native_list_empty()
|
|
let f4s: [Float] = native_list_empty()
|
|
let f5s: [Float] = native_list_empty()
|
|
let b1s: [Float] = native_list_empty()
|
|
let b2s: [Float] = native_list_empty()
|
|
let b3s: [Float] = native_list_empty()
|
|
let b4s: [Float] = native_list_empty()
|
|
let b5s: [Float] = native_list_empty()
|
|
|
|
let pos: Int = 0
|
|
while pos + frame_len <= n {
|
|
let raw: [Float] = dsp_slice(x, pos, frame_len)
|
|
let f0: Float = dsp_pitch_of(raw, sr)
|
|
if f0 > 0.0 {
|
|
f0s = native_list_append(f0s, f0)
|
|
let win: [Float] = dsp_hamming(raw)
|
|
let r: [Float] = dsp_autocorr(win, order)
|
|
let r0: Float = native_list_get(r, 0)
|
|
if r0 > 0.000001 {
|
|
let al: [Float] = dsp_levinson(r, order)
|
|
// strip the trailing residual energy: coeffs are a[0..order]
|
|
let a: [Float] = native_list_empty()
|
|
let ci: Int = 0
|
|
while ci <= order {
|
|
let av: Float = native_list_get(al, ci)
|
|
a = native_list_append(a, av)
|
|
ci = ci + 1
|
|
}
|
|
let fs: [Float] = dsp_formants(a, sr)
|
|
let nfs: Int = native_list_len(fs) / 2
|
|
if nfs > 0 {
|
|
let fv: Float = native_list_get(fs, 0)
|
|
let bv: Float = native_list_get(fs, 1)
|
|
f1s = native_list_append(f1s, fv)
|
|
b1s = native_list_append(b1s, bv)
|
|
}
|
|
if nfs > 1 {
|
|
let fv2: Float = native_list_get(fs, 2)
|
|
let bv2: Float = native_list_get(fs, 3)
|
|
f2s = native_list_append(f2s, fv2)
|
|
b2s = native_list_append(b2s, bv2)
|
|
}
|
|
if nfs > 2 {
|
|
let fv3: Float = native_list_get(fs, 4)
|
|
let bv3: Float = native_list_get(fs, 5)
|
|
f3s = native_list_append(f3s, fv3)
|
|
b3s = native_list_append(b3s, bv3)
|
|
}
|
|
if nfs > 3 {
|
|
let fv4: Float = native_list_get(fs, 6)
|
|
let bv4: Float = native_list_get(fs, 7)
|
|
f4s = native_list_append(f4s, fv4)
|
|
b4s = native_list_append(b4s, bv4)
|
|
}
|
|
if nfs > 4 {
|
|
let fv5: Float = native_list_get(fs, 8)
|
|
let bv5: Float = native_list_get(fs, 9)
|
|
f5s = native_list_append(f5s, fv5)
|
|
b5s = native_list_append(b5s, bv5)
|
|
}
|
|
}
|
|
}
|
|
pos = pos + hop
|
|
}
|
|
|
|
let f0med: Float = dsp_median(f0s)
|
|
let f0lo: Float = dsp_min_of(f0s)
|
|
let f0hi: Float = dsp_max_of(f0s)
|
|
out = native_list_append(out, f0med)
|
|
out = native_list_append(out, f0lo)
|
|
out = native_list_append(out, f0hi)
|
|
|
|
let nf: Int = 0
|
|
if native_list_len(f1s) > 0 {
|
|
nf = nf + 1
|
|
}
|
|
if native_list_len(f2s) > 0 {
|
|
nf = nf + 1
|
|
}
|
|
if native_list_len(f3s) > 0 {
|
|
nf = nf + 1
|
|
}
|
|
if native_list_len(f4s) > 0 {
|
|
nf = nf + 1
|
|
}
|
|
if native_list_len(f5s) > 0 {
|
|
nf = nf + 1
|
|
}
|
|
let nff: Float = int_to_float(nf)
|
|
out = native_list_append(out, nff)
|
|
if native_list_len(f1s) > 0 {
|
|
let m: Float = dsp_median(f1s)
|
|
let b: Float = dsp_median(b1s)
|
|
out = native_list_append(out, m)
|
|
out = native_list_append(out, b)
|
|
}
|
|
if native_list_len(f2s) > 0 {
|
|
let m2: Float = dsp_median(f2s)
|
|
let bb2: Float = dsp_median(b2s)
|
|
out = native_list_append(out, m2)
|
|
out = native_list_append(out, bb2)
|
|
}
|
|
if native_list_len(f3s) > 0 {
|
|
let m3: Float = dsp_median(f3s)
|
|
let bb3: Float = dsp_median(b3s)
|
|
out = native_list_append(out, m3)
|
|
out = native_list_append(out, bb3)
|
|
}
|
|
if native_list_len(f4s) > 0 {
|
|
let m4: Float = dsp_median(f4s)
|
|
let bb4: Float = dsp_median(b4s)
|
|
out = native_list_append(out, m4)
|
|
out = native_list_append(out, bb4)
|
|
}
|
|
if native_list_len(f5s) > 0 {
|
|
let m5: Float = dsp_median(f5s)
|
|
let bb5: Float = dsp_median(b5s)
|
|
out = native_list_append(out, m5)
|
|
out = native_list_append(out, bb5)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// imitate — LPC analysis-resynthesis. Per frame: autocorr + Levinson, gain =
|
|
// sqrt(residual energy), excitation = an energy-normalized glottal impulse
|
|
// train at F0 for voiced frames or white noise for unvoiced, run through the
|
|
// all-pole filter using the past-output state. The whole output is normalized
|
|
// to peak 0.9 and returned as int16 samples — hand them to speech.el's
|
|
// write_wav(samples, sr, path).
|
|
// ---------------------------------------------------------------------------
|
|
fn dsp_imitate(path: String) -> [Int] {
|
|
let res: [Int] = native_list_empty()
|
|
let w: [Float] = dsp_read_wav(path)
|
|
let n: Int = dsp_wav_n(w)
|
|
let frame_len: Int = 400
|
|
let hop: Int = 160
|
|
let order: Int = 16
|
|
if n <= frame_len {
|
|
return res
|
|
}
|
|
let sr: Int = dsp_wav_sr(w)
|
|
let srf: Float = int_to_float(sr)
|
|
let x: [Float] = dsp_wav_pcm(w)
|
|
|
|
let out: [Float] = native_list_empty()
|
|
// past outputs, order deep
|
|
let state: [Float] = native_list_empty()
|
|
let si: Int = 0
|
|
while si < order {
|
|
state = native_list_append(state, 0.0)
|
|
si = si + 1
|
|
}
|
|
let phase: Float = 0.0
|
|
let last_f0: Float = 0.0
|
|
let nstate: Int = 22695 // LCG for the unvoiced source
|
|
let written: Int = 0
|
|
|
|
let pos: Int = 0
|
|
while pos + frame_len <= n {
|
|
let raw: [Float] = dsp_slice(x, pos, frame_len)
|
|
let win: [Float] = dsp_hamming(raw)
|
|
let r: [Float] = dsp_autocorr(win, order)
|
|
let f0: Float = dsp_pitch_of(raw, sr)
|
|
let r0: Float = native_list_get(r, 0)
|
|
if r0 < 0.0000001 {
|
|
// frame skipped in periph.swift -> those output samples stay zero
|
|
let z: Int = 0
|
|
while z < hop {
|
|
if written < n {
|
|
out = native_list_append(out, 0.0)
|
|
written = written + 1
|
|
}
|
|
z = z + 1
|
|
}
|
|
} else {
|
|
let al: [Float] = dsp_levinson(r, order)
|
|
let errv: Float = native_list_get(al, order + 1)
|
|
let ge: Float = errv
|
|
if ge < 0.0 {
|
|
ge = 0.0
|
|
}
|
|
let gain: Float = math_sqrt(ge)
|
|
let use_f0: Float = f0
|
|
if f0 <= 0.0 {
|
|
use_f0 = 0.0
|
|
if last_f0 > 0.0 {
|
|
use_f0 = last_f0
|
|
}
|
|
}
|
|
last_f0 = f0
|
|
let i: Int = 0
|
|
while i < hop {
|
|
if written < n {
|
|
let e: Float = 0.0
|
|
if use_f0 > 0.0 {
|
|
phase = phase + use_f0 / srf
|
|
if phase >= 1.0 {
|
|
phase = phase - 1.0
|
|
e = math_sqrt(srf / use_f0)
|
|
}
|
|
} else {
|
|
nstate = nstate * 1103515245 + 12345
|
|
nstate = nstate - (nstate / 2147483648) * 2147483648
|
|
if nstate < 0 {
|
|
nstate = 0 - nstate
|
|
}
|
|
let u: Float = int_to_float(nstate) / 2147483648.0
|
|
e = u * 2.0 - 1.0
|
|
}
|
|
let y: Float = gain * e
|
|
let k: Int = 1
|
|
while k <= order {
|
|
let ak: Float = native_list_get(al, k)
|
|
let sk: Float = native_list_get(state, k - 1)
|
|
y = y - ak * sk
|
|
k = k + 1
|
|
}
|
|
let ns: [Float] = native_list_empty()
|
|
ns = native_list_append(ns, y)
|
|
let m: Int = 0
|
|
while m < order - 1 {
|
|
let sv: Float = native_list_get(state, m)
|
|
ns = native_list_append(ns, sv)
|
|
m = m + 1
|
|
}
|
|
state = ns
|
|
out = native_list_append(out, y)
|
|
written = written + 1
|
|
}
|
|
i = i + 1
|
|
}
|
|
}
|
|
pos = pos + hop
|
|
}
|
|
// the tail past the last full frame stays silent, exactly as in periph.swift
|
|
while written < n {
|
|
out = native_list_append(out, 0.0)
|
|
written = written + 1
|
|
}
|
|
|
|
// normalize to peak 0.9, then to int16
|
|
let peak: Float = 0.0
|
|
let q: Int = 0
|
|
while q < n {
|
|
let v: Float = native_list_get(out, q)
|
|
let av: Float = dsp_fabs(v)
|
|
if av > peak {
|
|
peak = av
|
|
}
|
|
q = q + 1
|
|
}
|
|
let scale: Float = 1.0
|
|
if peak > 0.000000001 {
|
|
scale = 0.9 / peak
|
|
}
|
|
let t: Int = 0
|
|
while t < n {
|
|
let v2: Float = native_list_get(out, t)
|
|
let sv2: Float = v2 * scale * 32767.0
|
|
if sv2 > 32767.0 {
|
|
sv2 = 32767.0
|
|
}
|
|
if sv2 < 0.0 - 32767.0 {
|
|
sv2 = 0.0 - 32767.0
|
|
}
|
|
let iv: Int = float_to_int(sv2)
|
|
res = native_list_append(res, iv)
|
|
t = t + 1
|
|
}
|
|
return res
|
|
}
|