Files
el/elp/src/speech.el
T
bigmerge b7e2c580a8
El SDK CI - dev / build-and-test (pull_request) Successful in 6m28s
Add native speech synthesis and voice-imitation faculty
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.
2026-08-15 14:27:52 -05:00

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)
}