b7e2c580a8
El SDK CI - dev / build-and-test (pull_request) Successful in 6m28s
speech.el: formant/glottal integer DSP synthesis + voice-analyze-by-
imitation. voice-profile.el / voice-ingest.el: voice-profile plumbing.
accent.el: British-RP as an ingested transform-geometry (explicitly marked
provisional/citation-pending by its own comments). organ-read.el:
engram read-through for the speech organ. Includes demo/test drivers and
non-personal reference data (British-RP phonetics/lexicon derived data,
a public-domain LibriVox RP reference recording).
Deliberately excludes elp/data/live/ (raw recorded voice + face-photo
samples of the repo owner) and the will-*.{json,psv} derived voiceprint
files — personal biometric data that shouldn't be committed to a shared
repo without an explicit decision from the owner. Also excludes this
worktree's elp/src/surface-profile.el, which diverges from the copy in
other worktrees (agent-aaf04b0a9714c4070, main) — needs manual
reconciliation before landing, left out here to avoid silently picking a
version.
461 lines
15 KiB
EmacsLisp
461 lines
15 KiB
EmacsLisp
// speech.el - The native SPEECH render path + voice-by-imitation extractor.
|
|
//
|
|
// Speech = the AUDIO surface (surface_profile_audio) rendering LANGUAGE-meaning
|
|
// through a VOICE signature. The realizer's language faculty supplies the words
|
|
// (meaning -> sem_realize -> text); this module turns text -> phonemes (phonetics.el)
|
|
// -> a formant-target track over time -> SUPERPOSES formant resonances over a
|
|
// glottal source (own-core formant synthesis, the exact integer mirror of the
|
|
// music additive superpose) -> own-core PCM/WAV. Two paths:
|
|
// (1) RENDER: speak(text, voice) -> spoken WAV.
|
|
// (2) IMITATE: voice_analyze(pcm) -> a voice signature grabbed BY EAR
|
|
// (autocorrelation pitch + integer-DFT formant peaks), then render
|
|
// any new meaning in that voice. An impression, not a corpus.
|
|
// All integer/fixed-point (EL float arithmetic is unusable).
|
|
|
|
// -- Own-core integer sine (Bhaskara I), phase 0..65535 = one cycle -----------
|
|
fn sp_sin(phase: Int) -> Int {
|
|
let deg: Int = phase * 360 / 65536
|
|
let neg: Int = 0
|
|
if deg > 180 {
|
|
deg = deg - 180
|
|
neg = 1
|
|
}
|
|
let t: Int = deg * (180 - deg)
|
|
let num: Int = 32767 * 4 * t
|
|
let den: Int = 40500 - t
|
|
let v: Int = num / den
|
|
if neg == 1 {
|
|
v = 0 - v
|
|
}
|
|
return v
|
|
}
|
|
|
|
fn sp_cos(phase: Int) -> Int {
|
|
let p: Int = phase + 16384
|
|
p = p - (p / 65536) * 65536
|
|
return sp_sin(p)
|
|
}
|
|
|
|
// One formant resonance (Lorentzian peak), Q15. Peak 32767 at f=fc.
|
|
fn sp_gain(f: Int, fc: Int, bw: Int) -> Int {
|
|
let d: Int = f - fc
|
|
let den: Int = d * d + bw * bw
|
|
let num: Int = 32767 * bw * bw
|
|
return num / den
|
|
}
|
|
|
|
fn sp_isqrt(n: Int) -> Int {
|
|
if n <= 0 {
|
|
return 0
|
|
}
|
|
let x: Int = n
|
|
let y: Int = (x + 1) / 2
|
|
while y < x {
|
|
x = y
|
|
y = (x + n / x) / 2
|
|
}
|
|
return x
|
|
}
|
|
|
|
// -- WAV serializer (thin medium; the only non-DSP glue) ---------------------
|
|
fn wav_le16(buf: String, off: Int, v: Int) -> String {
|
|
let u: Int = v
|
|
if u < 0 {
|
|
u = u + 65536
|
|
}
|
|
let lo: Int = u - (u / 256) * 256
|
|
let hi: Int = u / 256
|
|
let b: String = __str_set_char(buf, off, lo)
|
|
b = __str_set_char(b, off + 1, hi)
|
|
return b
|
|
}
|
|
|
|
fn wav_le32(buf: String, off: Int, v: Int) -> String {
|
|
let b0: Int = v - (v / 256) * 256
|
|
let r1: Int = v / 256
|
|
let b1: Int = r1 - (r1 / 256) * 256
|
|
let r2: Int = r1 / 256
|
|
let b2: Int = r2 - (r2 / 256) * 256
|
|
let b3: Int = r2 / 256
|
|
let b: String = __str_set_char(buf, off, b0)
|
|
b = __str_set_char(b, off + 1, b1)
|
|
b = __str_set_char(b, off + 2, b2)
|
|
b = __str_set_char(b, off + 3, b3)
|
|
return b
|
|
}
|
|
|
|
fn wav_ascii(buf: String, off: Int, s: String) -> String {
|
|
let n: Int = str_len(s)
|
|
let i: Int = 0
|
|
let b: String = buf
|
|
while i < n {
|
|
let c: Int = str_char_code(s, i)
|
|
b = __str_set_char(b, off + i, c)
|
|
i = i + 1
|
|
}
|
|
return b
|
|
}
|
|
|
|
fn write_wav(samples: [Int], sr: Int, path: String) -> Bool {
|
|
let ns: Int = native_list_len(samples)
|
|
let datalen: Int = ns * 2
|
|
let total: Int = 44 + datalen
|
|
let buf: String = __str_alloc(total)
|
|
buf = wav_ascii(buf, 0, "RIFF")
|
|
buf = wav_le32(buf, 4, 36 + datalen)
|
|
buf = wav_ascii(buf, 8, "WAVE")
|
|
buf = wav_ascii(buf, 12, "fmt ")
|
|
buf = wav_le32(buf, 16, 16)
|
|
buf = wav_le16(buf, 20, 1)
|
|
buf = wav_le16(buf, 22, 1)
|
|
buf = wav_le32(buf, 24, sr)
|
|
buf = wav_le32(buf, 28, sr * 2)
|
|
buf = wav_le16(buf, 32, 2)
|
|
buf = wav_le16(buf, 34, 16)
|
|
buf = wav_ascii(buf, 36, "data")
|
|
buf = wav_le32(buf, 40, datalen)
|
|
let j: Int = 0
|
|
let off: Int = 44
|
|
while j < ns {
|
|
let raw: Int = native_list_get(samples, j)
|
|
buf = wav_le16(buf, off, raw)
|
|
off = off + 2
|
|
j = j + 1
|
|
}
|
|
return __fs_write_bytes(path, buf, total)
|
|
}
|
|
|
|
// One formant resonance as a float Lorentzian peak (own-core physics).
|
|
fn fgain(f: Float, fc: Float, bw: Float) -> Float {
|
|
let d: Float = f - fc
|
|
return (bw * bw) / (d * d + bw * bw)
|
|
}
|
|
|
|
// His PITCH MELODY from measured prosody [f0_median, f0_min, f0_max, declination].
|
|
// A natural statement shape over the utterance: onset rise to the median, a
|
|
// near-flat body (his declination is ~0.6 Hz/s), and a final fall toward f0_min.
|
|
// Follows his melody + range, not a fixed 0.85 decline. gidx/total = position.
|
|
fn prosody_f0(pros: [Int], gidx: Int, total: Int) -> Int {
|
|
let med: Int = native_list_get(pros, 0)
|
|
let lo: Int = native_list_get(pros, 1)
|
|
let hi: Int = native_list_get(pros, 2)
|
|
let p: Int = gidx * 1000 / total
|
|
let f0: Int = med
|
|
if p < 150 {
|
|
f0 = lo + (med - lo) * p / 150
|
|
} else {
|
|
if p > 700 {
|
|
f0 = med + (lo - med) * (p - 700) / 300
|
|
} else {
|
|
f0 = med
|
|
}
|
|
}
|
|
if f0 < lo {
|
|
f0 = lo
|
|
}
|
|
if f0 > hi {
|
|
f0 = hi
|
|
}
|
|
return f0
|
|
}
|
|
|
|
// -- The render: phoneme codes + voice signature -> normalized PCM samples ----
|
|
// Formant geometry per phoneme is READ FROM THE ENGRAM (pmap) via phon_geo — no
|
|
// table in code. The optional ACCENT map (amap) composes a transform onto the
|
|
// voice (voice (+) accent, separable): RP formant overrides read from the accent
|
|
// manifold + a non-rhotic coda-R drop. Empty amap = base General-American.
|
|
// Synthesis is FLOAT: a real phase accumulator + math_sin, superposition physics.
|
|
fn synth_codes_accent(codes0: [String], voice: [String], pmap: [String], amap: [String], vset: [String], vmap: [String], prosody: [Int]) -> [Int] {
|
|
let sr: Int = 16000
|
|
let srf: Float = 16000.0
|
|
let two_pi: Float = 6.283185307
|
|
let kf: Int = voice_get_int(voice, "kf")
|
|
let f0s: Int = voice_get_int(voice, "f0")
|
|
let f0e: Int = voice_get_int(voice, "f0_end")
|
|
let durm: Int = voice_get_int(voice, "dur")
|
|
if kf <= 0 {
|
|
kf = 1000
|
|
}
|
|
if durm <= 0 {
|
|
durm = 1000
|
|
}
|
|
let use_accent: Int = 0
|
|
if native_list_len(amap) > 0 {
|
|
use_accent = 1
|
|
}
|
|
let codes: [String] = codes0
|
|
if use_accent == 1 {
|
|
if is_nonrhotic(amap) == 1 {
|
|
codes = apply_rhoticity(codes0, vset)
|
|
}
|
|
}
|
|
let nc: Int = native_list_len(codes)
|
|
|
|
// pass 1: per-segment sample counts + total
|
|
let segn: [Int] = native_list_empty()
|
|
let total: Int = 0
|
|
let ci: Int = 0
|
|
while ci < nc {
|
|
let code: String = native_list_get(codes, ci)
|
|
let p: [Int] = phon_geo(pmap, code)
|
|
let durms: Int = native_list_get(p, 8)
|
|
let ns: Int = durms * 16 * durm / 1000
|
|
segn = native_list_append(segn, ns)
|
|
total = total + ns
|
|
ci = ci + 1
|
|
}
|
|
if total <= 0 {
|
|
total = 1
|
|
}
|
|
|
|
// pass 2: synthesize
|
|
let samples: [Int] = native_list_empty()
|
|
let phasef: Float = 0.0
|
|
let gidx: Int = 0
|
|
let prevF1: Int = 500 * kf / 1000
|
|
let prevF2: Int = 1500 * kf / 1000
|
|
let prevF3: Int = 2500 * kf / 1000
|
|
let nstate: Int = 22695
|
|
let maxabs: Int = 1
|
|
|
|
let ci2: Int = 0
|
|
while ci2 < nc {
|
|
let code: String = native_list_get(codes, ci2)
|
|
let p: [Int] = phon_geo(pmap, code)
|
|
let rf1: Int = native_list_get(p, 0)
|
|
let rf2: Int = native_list_get(p, 1)
|
|
let rf3: Int = native_list_get(p, 2)
|
|
if use_accent == 1 {
|
|
let ov: [Int] = accent_formants(amap, code)
|
|
if native_list_len(ov) >= 3 {
|
|
rf1 = native_list_get(ov, 0)
|
|
rf2 = native_list_get(ov, 1)
|
|
rf3 = native_list_get(ov, 2)
|
|
}
|
|
}
|
|
// HIS measured vowel target overrides the generic/kf path (absolute Hz —
|
|
// his formants already encode his vocal tract, so no kf scaling).
|
|
let usekf: Int = 1
|
|
if native_list_len(vmap) > 0 {
|
|
let hv: [Int] = vmap_get(vmap, code)
|
|
if native_list_len(hv) >= 3 {
|
|
rf1 = native_list_get(hv, 0)
|
|
rf2 = native_list_get(hv, 1)
|
|
rf3 = native_list_get(hv, 2)
|
|
usekf = 0
|
|
}
|
|
}
|
|
let F1t: Int = rf1 * kf / 1000
|
|
let F2t: Int = rf2 * kf / 1000
|
|
let F3t: Int = rf3 * kf / 1000
|
|
if usekf == 0 {
|
|
F1t = rf1
|
|
F2t = rf2
|
|
F3t = rf3
|
|
}
|
|
let B1: Int = native_list_get(p, 3)
|
|
let B2: Int = native_list_get(p, 4)
|
|
let B3: Int = native_list_get(p, 5)
|
|
let voiced: Int = native_list_get(p, 6)
|
|
let ampv: Int = native_list_get(p, 9)
|
|
let ns: Int = native_list_get(segn, ci2)
|
|
let trans: Int = ns / 2
|
|
if trans > 560 {
|
|
trans = 560
|
|
}
|
|
if trans < 1 {
|
|
trans = 1
|
|
}
|
|
let k: Int = 0
|
|
while k < ns {
|
|
let cF1: Int = F1t
|
|
let cF2: Int = F2t
|
|
let cF3: Int = F3t
|
|
if k < trans {
|
|
cF1 = prevF1 + (F1t - prevF1) * k / trans
|
|
cF2 = prevF2 + (F2t - prevF2) * k / trans
|
|
cF3 = prevF3 + (F3t - prevF3) * k / trans
|
|
}
|
|
let f0c: Int = f0s + (f0e - f0s) * gidx / total
|
|
if native_list_len(prosody) >= 3 {
|
|
f0c = prosody_f0(prosody, gidx, total)
|
|
}
|
|
if f0c < 40 {
|
|
f0c = 40
|
|
}
|
|
let env: Int = 32767
|
|
let ar: Int = 96
|
|
if k < ar {
|
|
env = 32767 * k / ar
|
|
}
|
|
let tail: Int = ns - k
|
|
if tail < ar {
|
|
env = 32767 * tail / ar
|
|
}
|
|
let f0cf: Float = int_to_float(f0c)
|
|
phasef = phasef + two_pi * f0cf / srf
|
|
if phasef > two_pi {
|
|
phasef = phasef - two_pi
|
|
}
|
|
|
|
let s: Int = 0
|
|
if voiced == 1 {
|
|
let cF1f: Float = int_to_float(cF1)
|
|
let cF2f: Float = int_to_float(cF2)
|
|
let cF3f: Float = int_to_float(cF3)
|
|
let B1f: Float = int_to_float(B1)
|
|
let B2f: Float = int_to_float(B2)
|
|
let B3f: Float = int_to_float(B3)
|
|
let acc: Float = 0.0
|
|
let h: Int = 1
|
|
while h <= 50 {
|
|
let hf: Float = int_to_float(h)
|
|
let fhf: Float = hf * f0cf
|
|
if fhf < 7900.0 {
|
|
let sv: Float = math_sin(phasef * hf)
|
|
let src: Float = 1.0 / hf
|
|
let g1: Float = fgain(fhf, cF1f, B1f)
|
|
let g2: Float = fgain(fhf, cF2f, B2f)
|
|
let g3: Float = fgain(fhf, cF3f, B3f)
|
|
let g: Float = g1 + g2 + g3
|
|
acc = acc + src * g * sv
|
|
}
|
|
h = h + 1
|
|
}
|
|
s = float_to_int(acc * 4000.0)
|
|
} else {
|
|
if ampv > 0 {
|
|
nstate = nstate * 1103515245 + 12345
|
|
nstate = nstate - (nstate / 2147483648) * 2147483648
|
|
if nstate < 0 {
|
|
nstate = 0 - nstate
|
|
}
|
|
let nz: Int = nstate / 32768 - 32768
|
|
s = nz
|
|
}
|
|
}
|
|
s = s * ampv / 100
|
|
s = s * env / 32767
|
|
samples = native_list_append(samples, s)
|
|
let a: Int = s
|
|
if a < 0 {
|
|
a = 0 - a
|
|
}
|
|
if a > maxabs {
|
|
maxabs = a
|
|
}
|
|
gidx = gidx + 1
|
|
k = k + 1
|
|
}
|
|
prevF1 = F1t
|
|
prevF2 = F2t
|
|
prevF3 = F3t
|
|
ci2 = ci2 + 1
|
|
}
|
|
|
|
// normalize to int16 range (~22000 peak)
|
|
let out: [Int] = native_list_empty()
|
|
let ntot: Int = native_list_len(samples)
|
|
let j: Int = 0
|
|
while j < ntot {
|
|
let raw: Int = native_list_get(samples, j)
|
|
let v: Int = raw * 22000 / maxabs
|
|
out = native_list_append(out, v)
|
|
j = j + 1
|
|
}
|
|
return out
|
|
}
|
|
|
|
// GA convenience wrapper (no accent) — keeps the base render path.
|
|
fn synth_codes(codes: [String], voice: [String], pmap: [String]) -> [Int] {
|
|
let noacc: [String] = native_list_empty()
|
|
let novset: [String] = native_list_empty()
|
|
let novmap: [String] = native_list_empty()
|
|
let nopros: [Int] = native_list_empty()
|
|
return synth_codes_accent(codes, voice, pmap, noacc, novset, novmap, nopros)
|
|
}
|
|
|
|
// -- Voice-by-imitation: HEAR a PCM sample -> extract the voice signature -----
|
|
// Pitch by autocorrelation; vocal-tract scale (kf) from the F1 formant peak of a
|
|
// heard sustained vowel /AA/ (nominal F1 = 730 Hz) via an integer DFT. The
|
|
// analyzer sees ONLY the PCM samples — never the source signature numbers — so
|
|
// recovery is genuinely by ear.
|
|
fn voice_f0(samples: [Int], sr: Int) -> Int {
|
|
let n: Int = native_list_len(samples)
|
|
let start: Int = n / 4
|
|
let end: Int = n * 3 / 4
|
|
// bound the analysis window so accumulators can never overflow on long input
|
|
if end - start > 6000 {
|
|
end = start + 6000
|
|
}
|
|
let minlag: Int = sr / 300
|
|
let maxlag: Int = sr / 75
|
|
let best: Int = 0
|
|
let bestlag: Int = minlag
|
|
let lag: Int = minlag
|
|
while lag <= maxlag {
|
|
let sum: Int = 0
|
|
let i: Int = start
|
|
while i < end {
|
|
let ai: Int = native_list_get(samples, i)
|
|
let bi: Int = native_list_get(samples, i + lag)
|
|
sum = sum + ai * bi / 256
|
|
i = i + 2
|
|
}
|
|
if sum > best {
|
|
best = sum
|
|
bestlag = lag
|
|
}
|
|
lag = lag + 1
|
|
}
|
|
if bestlag < 1 {
|
|
bestlag = 1
|
|
}
|
|
return sr / bestlag
|
|
}
|
|
|
|
fn voice_peak_in_band(samples: [Int], sr: Int, flo: Int, fhi: Int) -> Int {
|
|
let n: Int = native_list_len(samples)
|
|
let start: Int = n / 4
|
|
let end: Int = n * 3 / 4
|
|
// bound the DFT window: re/im are accumulated /4096, and re*re must stay in
|
|
// int64 — cap terms so (window/2)*(peak_term) squared cannot overflow.
|
|
if end - start > 3000 {
|
|
end = start + 3000
|
|
}
|
|
let bestmag: Int = 0
|
|
let bestf: Int = flo
|
|
let f: Int = flo
|
|
while f <= fhi {
|
|
let re: Int = 0
|
|
let im: Int = 0
|
|
let i: Int = start
|
|
while i < end {
|
|
let x: Int = native_list_get(samples, i)
|
|
let ph: Int = i * f * 65536 / sr
|
|
ph = ph - (ph / 65536) * 65536
|
|
let cq: Int = sp_cos(ph)
|
|
let sq: Int = sp_sin(ph)
|
|
re = re + x * cq / 4096
|
|
im = im + x * sq / 4096
|
|
i = i + 2
|
|
}
|
|
let mag: Int = re * re + im * im
|
|
if mag > bestmag {
|
|
bestmag = mag
|
|
bestf = f
|
|
}
|
|
f = f + 25
|
|
}
|
|
return bestf
|
|
}
|
|
|
|
// Analyze a heard sustained /AA/ -> a full voice signature (by ear).
|
|
fn voice_analyze(samples: [Int], sr: Int) -> [String] {
|
|
let f0: Int = voice_f0(samples, sr)
|
|
let f1: Int = voice_peak_in_band(samples, sr, 450, 1150)
|
|
let kf: Int = 1000 * f1 / 730
|
|
let f0e: Int = f0 * 85 / 100
|
|
return voice_new("imitated", f0, f0e, kf, 1000, 1000, 8)
|
|
}
|