4bbfdcceff
audio-surface.el / image-surface.el: own-core additive-synthesis WAV and
raster-PNG renderers (integer-only DSP, since EL has no floats), rendered
from learned engram signatures via a pluggable surface-profile
abstraction (surface-profile.el). audio-demo.el / image-demo.el are
drivers. NOTE: demo files hardcode absolute paths to this worktree's own
directory — will need a path fixup before landing.
elp/projector/ is a Python package the author's own README marks as
"STAGING/PROOF-OF-SHAPE — not the deliverable", superseded by the native
.el surface-profile work above; kept as a validated architecture proof.
Generated output (elp/faculty/{out,sig}, elp/projector/out,
__pycache__) intentionally excluded.
401 lines
14 KiB
EmacsLisp
401 lines
14 KiB
EmacsLisp
// audio-surface.el - Native own-core additive-synthesis audio surface.
|
|
//
|
|
// The AUDIO efferent seam, native, no Python and no library. This renders real
|
|
// PCM .wav bytes from instrument SIGNATURES read from engram-sourced .sig data
|
|
// files (elp/faculty/sig/*.sig) - the partial amplitudes are NEVER literals in
|
|
// this source; they are parsed from the learned signature at run time. That is
|
|
// the whole proof: render-from-learned-signatures.
|
|
//
|
|
// EL has no float arithmetic operator (codegen emits raw int64 ops for + - * /
|
|
// on the shared 64-bit slot) and no float-arithmetic natives - so ALL synthesis
|
|
// math here is own-core INTEGER fixed-point. Angles use a quarter-wave sine
|
|
// table (scale 10000) from a fixed-point Taylor series; amplitudes are parsed to
|
|
// micro (scale 1e6) straight from the .sig text; frequencies are milliHz ints.
|
|
//
|
|
// Pipeline mirrors the two-stage projector (midi.py): plan_note(frame) reads a
|
|
// frame's meaning-geometry slot-map and derives (pitch, duration, amplitude);
|
|
// realize_audio SUPERPOSES the signature's partials (the compose op) and
|
|
// serialises RIFF/WAVE. Same frame -> midi OR audio.
|
|
|
|
// -- integer decimal + string helpers -----------------------------------------
|
|
|
|
fn str_to_int_el(s: String) -> Int {
|
|
let n: Int = str_len(s)
|
|
let i: Int = 0
|
|
let v: Int = 0
|
|
let neg: Bool = false
|
|
while i < n {
|
|
let c: Int = str_char_code(s, i)
|
|
if c == 45 { let neg: Bool = true }
|
|
if c >= 48 {
|
|
if c < 58 {
|
|
let v: Int = v * 10 + (c - 48)
|
|
}
|
|
}
|
|
let i: Int = i + 1
|
|
}
|
|
if neg { return 0 - v }
|
|
return v
|
|
}
|
|
|
|
fn parse_micro(s: String) -> Int {
|
|
let dot: Int = str_index_of(s, ".")
|
|
if dot < 0 {
|
|
return str_to_int_el(s) * 1000000
|
|
}
|
|
let n: Int = str_len(s)
|
|
let ipart: String = str_slice(s, 0, dot)
|
|
let fpart: String = str_slice(s, dot + 1, n)
|
|
let iv: Int = str_to_int_el(ipart)
|
|
let fv: Int = 0
|
|
let scale: Int = 100000
|
|
let fn2: Int = str_len(fpart)
|
|
let i: Int = 0
|
|
while i < 6 {
|
|
let d: Int = 0
|
|
if i < fn2 {
|
|
let d: Int = str_char_code(fpart, i) - 48
|
|
}
|
|
let fv: Int = fv + d * scale
|
|
let scale: Int = scale / 10
|
|
let i: Int = i + 1
|
|
}
|
|
return iv * 1000000 + fv
|
|
}
|
|
|
|
// -- signature (engram data file) loader ---------------------------------------
|
|
|
|
fn sig_load(path: String) -> [String] {
|
|
let text: String = fs_read(path)
|
|
return str_split(text, "\n")
|
|
}
|
|
|
|
fn sig_field(lines: [String], key: String) -> String {
|
|
let pref: String = key + ": "
|
|
let n: Int = native_list_len(lines)
|
|
let plen: Int = str_len(pref)
|
|
let i: Int = 0
|
|
while i < n {
|
|
let ln: String = native_list_get(lines, i)
|
|
if str_starts_with(ln, pref) {
|
|
return str_slice(ln, plen, str_len(ln))
|
|
}
|
|
let i: Int = i + 1
|
|
}
|
|
return ""
|
|
}
|
|
|
|
fn parse_micros(csv: String) -> [Int] {
|
|
let parts: [String] = str_split(csv, ",")
|
|
let n: Int = native_list_len(parts)
|
|
let out: [Int] = native_list_empty()
|
|
let i: Int = 0
|
|
while i < n {
|
|
let out: [Int] = native_list_append(out, parse_micro(native_list_get(parts, i)))
|
|
let i: Int = i + 1
|
|
}
|
|
return out
|
|
}
|
|
|
|
// -- fixed-point sine (own-core, quarter-wave Taylor table, scale 10000) --------
|
|
|
|
fn sin_table() -> [Int] {
|
|
let HP: Int = 1570796
|
|
let t: [Int] = native_list_empty()
|
|
let q: Int = 0
|
|
while q < 257 {
|
|
let x: Int = q * HP / 256
|
|
let x2: Int = x * x / 1000000
|
|
let x3: Int = x2 * x / 1000000
|
|
let x5: Int = x3 * x2 / 1000000
|
|
let x7: Int = x5 * x2 / 1000000
|
|
let x9: Int = x7 * x2 / 1000000
|
|
let s: Int = x - x3 / 6 + x5 / 120 - x7 / 5040 + x9 / 362880
|
|
let t: [Int] = native_list_append(t, s / 100)
|
|
let q: Int = q + 1
|
|
}
|
|
return t
|
|
}
|
|
|
|
fn sin_lookup(t: [Int], phase: Int) -> Int {
|
|
let p: Int = phase % 1024
|
|
if p < 0 { let p: Int = p + 1024 }
|
|
let quad: Int = p / 256
|
|
let r: Int = p % 256
|
|
if quad == 0 { return native_list_get(t, r) }
|
|
if quad == 1 { return native_list_get(t, 256 - r) }
|
|
if quad == 2 { return 0 - native_list_get(t, r) }
|
|
return 0 - native_list_get(t, 256 - r)
|
|
}
|
|
|
|
fn isqrt_int(n: Int) -> Int {
|
|
if n <= 0 { return 0 }
|
|
let x: Int = n
|
|
let y: Int = (x + 1) / 2
|
|
while y < x {
|
|
let x: Int = y
|
|
let y: Int = (x + n / x) / 2
|
|
}
|
|
return x
|
|
}
|
|
|
|
// freq_of_midi: equal-tempered frequency in milliHz. 440000 mHz at midi 69.
|
|
fn freq_of_midi(m: Int) -> Int {
|
|
let f: Int = 440000
|
|
if m > 69 {
|
|
let k: Int = m - 69
|
|
let i: Int = 0
|
|
while i < k {
|
|
let f: Int = f * 1059463 / 1000000
|
|
let i: Int = i + 1
|
|
}
|
|
return f
|
|
}
|
|
if m < 69 {
|
|
let k: Int = 69 - m
|
|
let i: Int = 0
|
|
while i < k {
|
|
let f: Int = f * 1000000 / 1059463
|
|
let i: Int = i + 1
|
|
}
|
|
return f
|
|
}
|
|
return f
|
|
}
|
|
|
|
// -- envelope (ADSR), scale 1000 -----------------------------------------------
|
|
|
|
fn adsr_env(i: Int, total: Int, atk_n: Int, dec_n: Int, sus_pm: Int, rel_n: Int) -> Int {
|
|
if i < atk_n {
|
|
if atk_n == 0 { return 1000 }
|
|
return 1000 * i / atk_n
|
|
}
|
|
if i < atk_n + dec_n {
|
|
if dec_n == 0 { return sus_pm }
|
|
return 1000 - (1000 - sus_pm) * (i - atk_n) / dec_n
|
|
}
|
|
let rel_start: Int = total - rel_n
|
|
if i < rel_start {
|
|
return sus_pm
|
|
}
|
|
if rel_n == 0 { return 0 }
|
|
let left: Int = total - i
|
|
return sus_pm * left / rel_n
|
|
}
|
|
|
|
// -- note synthesis: SUPERPOSE the learned partials -> [Int] samples -----------
|
|
fn note_samples(freq_mHz: Int, dur_ms: Int, rate: Int, partials: [Int], sumP: Int, b_micro: Int, vib_rate: Int, vib_cents: Int, atk_ms: Int, dec_ms: Int, sus_pm: Int, rel_ms: Int, amp_pm: Int, table: [Int]) -> [Int] {
|
|
let total: Int = dur_ms * rate / 1000
|
|
let atk_n: Int = atk_ms * rate / 1000
|
|
let dec_n: Int = dec_ms * rate / 1000
|
|
let rel_n: Int = rel_ms * rate / 1000
|
|
let np: Int = native_list_len(partials)
|
|
let half_mhz: Int = rate * 1000 / 2
|
|
let out: [Int] = native_list_empty()
|
|
let i: Int = 0
|
|
while i < total {
|
|
let acc: Int = 0
|
|
let k: Int = 0
|
|
while k < np {
|
|
let harm: Int = k + 1
|
|
let amp_k: Int = native_list_get(partials, k)
|
|
let factor: Int = 1000000
|
|
if b_micro > 0 {
|
|
let val: Int = 1000000 + b_micro * harm * harm
|
|
let factor: Int = isqrt_int(val * 1000000)
|
|
}
|
|
let fn_mhz: Int = freq_mHz * harm
|
|
let fn_mhz: Int = fn_mhz * factor / 1000000
|
|
if vib_cents > 0 {
|
|
if vib_rate > 0 {
|
|
let vphase: Int = i * vib_rate * 1024 / rate
|
|
let vs: Int = sin_lookup(table, vphase)
|
|
let vibf: Int = 1000000 + (vib_cents * vs * 833) / 10000
|
|
let fn_mhz: Int = fn_mhz * vibf / 1000000
|
|
}
|
|
}
|
|
if fn_mhz <= half_mhz {
|
|
let phase: Int = i * fn_mhz * 1024 / (rate * 1000)
|
|
let sv: Int = sin_lookup(table, phase)
|
|
let acc: Int = acc + sv * amp_k / 1000000
|
|
}
|
|
let k: Int = k + 1
|
|
}
|
|
let env: Int = adsr_env(i, total, atk_n, dec_n, sus_pm, rel_n)
|
|
let s16: Int = acc * 2800000 / sumP
|
|
let s16: Int = s16 * env / 1000
|
|
let s16: Int = s16 * amp_pm / 1000
|
|
if s16 > 32767 { let s16: Int = 32767 }
|
|
if s16 < 0 - 32767 { let s16: Int = 0 - 32767 }
|
|
let out: [Int] = native_list_append(out, s16)
|
|
let i: Int = i + 1
|
|
}
|
|
return out
|
|
}
|
|
|
|
fn synth_from_sig(lines: [String], freq_mHz: Int, dur_ms: Int, amp_pm: Int, rate: Int, table: [Int]) -> [Int] {
|
|
let partials: [Int] = parse_micros(sig_field(lines, "partials"))
|
|
let np: Int = native_list_len(partials)
|
|
let sumP: Int = 0
|
|
let j: Int = 0
|
|
while j < np {
|
|
let pj: Int = native_list_get(partials, j)
|
|
let sumP: Int = sumP + pj
|
|
let j: Int = j + 1
|
|
}
|
|
if sumP <= 0 { let sumP: Int = 1000000 }
|
|
let adsr: [String] = str_split(sig_field(lines, "adsr"), ",")
|
|
let atk_ms: Int = parse_micro(native_list_get(adsr, 0)) / 1000
|
|
let dec_ms: Int = parse_micro(native_list_get(adsr, 1)) / 1000
|
|
let sus_pm: Int = parse_micro(native_list_get(adsr, 2)) / 1000
|
|
let rel_ms: Int = parse_micro(native_list_get(adsr, 3)) / 1000
|
|
let b_micro: Int = parse_micro(sig_field(lines, "inharmonicity_B"))
|
|
let vib_rate: Int = str_to_int_el(sig_field(lines, "vibrato_rate_hz"))
|
|
let vib_cents: Int = str_to_int_el(sig_field(lines, "vibrato_depth_cents"))
|
|
return note_samples(freq_mHz, dur_ms, rate, partials, sumP, b_micro, vib_rate, vib_cents, atk_ms, dec_ms, sus_pm, rel_ms, amp_pm, table)
|
|
}
|
|
|
|
// -- byte-buffer helpers (own-core, no library) --------------------------------
|
|
|
|
fn put_tag(buf: String, pos: Int, s: String) -> String {
|
|
let n: Int = str_len(s)
|
|
let i: Int = 0
|
|
while i < n {
|
|
let buf: String = __str_set_char(buf, pos + i, str_char_code(s, i))
|
|
let i: Int = i + 1
|
|
}
|
|
return buf
|
|
}
|
|
|
|
fn put_u32le(buf: String, pos: Int, v: Int) -> String {
|
|
let buf: String = __str_set_char(buf, pos, v % 256)
|
|
let buf: String = __str_set_char(buf, pos + 1, (v / 256) % 256)
|
|
let buf: String = __str_set_char(buf, pos + 2, (v / 65536) % 256)
|
|
let buf: String = __str_set_char(buf, pos + 3, (v / 16777216) % 256)
|
|
return buf
|
|
}
|
|
|
|
fn put_u16le(buf: String, pos: Int, v: Int) -> String {
|
|
let buf: String = __str_set_char(buf, pos, v % 256)
|
|
let buf: String = __str_set_char(buf, pos + 1, (v / 256) % 256)
|
|
return buf
|
|
}
|
|
|
|
// -- WAV serializer: own-core RIFF/WAVE, PCM mono 16-bit -----------------------
|
|
|
|
fn wav_write(path: String, samples: [Int], n: Int, rate: Int) -> Int {
|
|
let data_len: Int = n * 2
|
|
let total: Int = 44 + data_len
|
|
let buf: String = __str_alloc(total)
|
|
let buf: String = put_tag(buf, 0, "RIFF")
|
|
let buf: String = put_u32le(buf, 4, 36 + data_len)
|
|
let buf: String = put_tag(buf, 8, "WAVE")
|
|
let buf: String = put_tag(buf, 12, "fmt ")
|
|
let buf: String = put_u32le(buf, 16, 16)
|
|
let buf: String = put_u16le(buf, 20, 1)
|
|
let buf: String = put_u16le(buf, 22, 1)
|
|
let buf: String = put_u32le(buf, 24, rate)
|
|
let buf: String = put_u32le(buf, 28, rate * 2)
|
|
let buf: String = put_u16le(buf, 32, 2)
|
|
let buf: String = put_u16le(buf, 34, 16)
|
|
let buf: String = put_tag(buf, 36, "data")
|
|
let buf: String = put_u32le(buf, 40, data_len)
|
|
let i: Int = 0
|
|
while i < n {
|
|
let v: Int = native_list_get(samples, i)
|
|
if v < 0 { let v: Int = v + 65536 }
|
|
let buf: String = __str_set_char(buf, 44 + i * 2, v % 256)
|
|
let buf: String = __str_set_char(buf, 44 + i * 2 + 1, (v / 256) % 256)
|
|
let i: Int = i + 1
|
|
}
|
|
let ok: Int = fs_write_bytes(path, buf, total)
|
|
return ok
|
|
}
|
|
|
|
// -- plan: frame slot-map -> note atom (pitch, duration, amplitude) ------------
|
|
|
|
fn audio_frame(relation: String, polarity: String, confidence: String, importance: String, salience: String, subj_id: String) -> [String] {
|
|
let f: [String] = native_list_empty()
|
|
let f: [String] = native_list_append(f, "relation")
|
|
let f: [String] = native_list_append(f, relation)
|
|
let f: [String] = native_list_append(f, "polarity")
|
|
let f: [String] = native_list_append(f, polarity)
|
|
let f: [String] = native_list_append(f, "confidence")
|
|
let f: [String] = native_list_append(f, confidence)
|
|
let f: [String] = native_list_append(f, "importance")
|
|
let f: [String] = native_list_append(f, importance)
|
|
let f: [String] = native_list_append(f, "salience")
|
|
let f: [String] = native_list_append(f, salience)
|
|
let f: [String] = native_list_append(f, "subj_id")
|
|
let f: [String] = native_list_append(f, subj_id)
|
|
return f
|
|
}
|
|
|
|
fn degree_offset(deg: Int) -> Int {
|
|
if deg == 0 { return 0 }
|
|
if deg == 1 { return 2 }
|
|
if deg == 2 { return 4 }
|
|
if deg == 3 { return 5 }
|
|
if deg == 4 { return 7 }
|
|
if deg == 5 { return 9 }
|
|
return 11
|
|
}
|
|
|
|
// returns [midi, dur_ms, amp_pm]
|
|
fn plan_note(frame: [String]) -> [Int] {
|
|
let relation: String = surface_get(frame, "relation")
|
|
let polarity: String = surface_get(frame, "polarity")
|
|
let confidence: String = surface_get(frame, "confidence")
|
|
let importance: String = surface_get(frame, "importance")
|
|
let salience: String = surface_get(frame, "salience")
|
|
let rn: Int = str_len(relation)
|
|
let csum: Int = 0
|
|
let i: Int = 0
|
|
while i < rn {
|
|
let cc: Int = str_char_code(relation, i)
|
|
let csum: Int = csum + cc
|
|
let i: Int = i + 1
|
|
}
|
|
let deg: Int = csum % 7
|
|
let third: Int = 4
|
|
if str_eq(polarity, "neg") { let third: Int = 3 }
|
|
let sal_oct: Int = str_to_int_el(salience)
|
|
let doff: Int = degree_offset(deg)
|
|
let midi: Int = 60 + sal_oct * 12 + doff + third
|
|
let conf_micro: Int = parse_micro(confidence)
|
|
let dur_ms: Int = 200 + conf_micro / 1000
|
|
let imp_micro: Int = parse_micro(importance)
|
|
let amp_pm: Int = 400 + imp_micro / 2000
|
|
let out: [Int] = native_list_empty()
|
|
let out: [Int] = native_list_append(out, midi)
|
|
let out: [Int] = native_list_append(out, dur_ms)
|
|
let out: [Int] = native_list_append(out, amp_pm)
|
|
return out
|
|
}
|
|
|
|
fn realize_audio(frames: [[String]], sig_lines: [String], path: String, rate: Int, table: [Int]) -> Int {
|
|
let nf: Int = native_list_len(frames)
|
|
let all: [Int] = native_list_empty()
|
|
let count: Int = 0
|
|
let fi: Int = 0
|
|
while fi < nf {
|
|
let frame: [String] = native_list_get(frames, fi)
|
|
let plan: [Int] = plan_note(frame)
|
|
let midi: Int = native_list_get(plan, 0)
|
|
let dur_ms: Int = native_list_get(plan, 1)
|
|
let amp_pm: Int = native_list_get(plan, 2)
|
|
let freq: Int = freq_of_midi(midi)
|
|
let note: [Int] = synth_from_sig(sig_lines, freq, dur_ms, amp_pm, rate, table)
|
|
let nn: Int = native_list_len(note)
|
|
let j: Int = 0
|
|
while j < nn {
|
|
let all: [Int] = native_list_append(all, native_list_get(note, j))
|
|
let j: Int = j + 1
|
|
}
|
|
let count: Int = count + nn
|
|
let fi: Int = fi + 1
|
|
}
|
|
let ok: Int = wav_write(path, all, count, rate)
|
|
return count
|
|
}
|