Compare commits

...
3 Commits
3 changed files with 486 additions and 0 deletions
+155
View File
@@ -2746,3 +2746,158 @@ Our application: mask × audio via complex multiply (WITH 1/N from IFFT)
1. Исправить mask computation (заменить LUT на raw_level или калибровать) 1. Исправить mask computation (заменить LUT на raw_level или калибровать)
2. Реализовать FIR convolution (OLA) вместо per-bin multiply 2. Реализовать FIR convolution (OLA) вместо per-bin multiply
3. Оба исправления требуются для bit-exact 3. Оба исправления требуются для bit-exact
## ============ UPDATE 2026-08-24j (24j): ПРИМЕНЁННЫЙ ОТКЛИК = ЖИВОЙ FIR ПОТЕНЦИАЛЬНО; ×1.805 = ФАНТОМ STALE-КЕРНЕЛА ============
### Метод (новый канал, скрипты rendersnap2.py + генератор зондов)
1. МЯГКИЙ снаппер `scripts/rendersnap2.py`: НЕ убивает reaper (частичный-wav
hazard 24c обойдён — рендер доестественного выхода), дополнительно дампит
банк скаляров ctx+0x540860..0x5408c0 каждый фазой.
2. Мультитон-проект: клоны dual_b1q_0.5.rpp с входом «2 главных тона 500+2000
@0.4248 (= амплитуды dual.wav) + 20 зондов −30 дБ на сетке 100..7200 Гц».
Детектор в том же адаптивном состоянии, H измерима Goertzel'ем на всех
зондах. Генерация inline (/tmp/opencode/multi_in.wav + multi.rpp).
3. Свип НЕ годится для Y/X-отношения (энергия на гребне) — только мультитон.
### Результат multi_b1q_0.5 (живой FIR ph069: m@43=0.320, m@171=0.267)
| f, Гц | cut дБ | gain | m(FIR) | gain/m |
|-------|--------|------|--------|--------|
| 500 | 10.32 | 0.3048 | 0.3234 | 0.943 |
| 2000 | 11.84 | 0.2558 | 0.2698 | 0.948 |
| 700..7200 (18 зондов) | 1.94.6 | — | — | **0.981.00** |
(450/550 аномальны — утечка боковых лепестков окна Гёрцеля от главного
тона 500; не в счёт.) Контрольные числа воспроизведены: 10.32/11.84 ✓✓.
### ВЫВОДЫ (три, все жёсткие)
1. **Применённый отклик = mag(живого FIR@540668) побиновно** (±2%; главные
тона −0.5 дБ — недозахват глубины между фазами). НИКАКОЙ дополнительной
свёртки/OLA/×1.805 поверх per-bin multiply НЕ СУЩЕСТВУЕТ.
2. **«Свёртка ×1.805» (23e) и «OLA-нормализация» (24i) ОТЗЫВАЮТСЯ**: источник —
сравнение cut живого рендера с init-time кернелом. Старые phase*.npz из
rendersnap-сессий держат m@43=0.588 СЕКУНДАМИ — это ядро после param-rebuild
при ЗАГРУЗКЕ (52e9b0 строит FIR из init-state bands[]); живой рендер
перестраивает FIR КАЖДЫЙ кадр и мгновенно потребляет → STOP-семпл ловит
identity/reset (все 110 фаз sweep-рендера ≈ 1.0). 0.305/0.32≈0.95,
0.524(init)/0.32(live)=1.64~«1.805» — вот вся «константа».
3. **R-слоты 0x5407f8/0x540788 — СТАТИКА**: r43=3.9807/r43=4.0285 ИДЕНТИЧНЫ во
всех трёх проектах (dual/sweep/multi, разный контент). Это twin-шаблон
полосы (геометрия), НЕ динамическая маска. Интерпретация 22y «R=1/mask
живая кривая редукции» — снята (кривая была от инициализации).
4. **Скаляр [ctx+0x540888]=1.000000** весь рендер (и s874/s87c/s88c=1.0;
s870=440.955=sens-scale). Wet-скаляр BLOCKMAP 23b НЕ вносит масштаба при
дефолтных параметрах. ×N-нормировка FFT (24g) тоже не подтверждается:
применённый gain совпадает с mag(FIR) без поправок.
### Что это меняет в архитектуре транскрипции
- RT_FIRCONV как «особое применение» НЕ нужен: достаточно per-bin multiply
спектра кадра на маску-кривую (как default-путь spectral.cpp).
- ВСЕ усилия — в ФОРМУ кривой: bands[i] → (log→ops A-D→exp0.984→hannfall→ops)
→ FIR. Наш structural chain должен выдать кривую, чья mag совпадёт с живым
FIR фазы. Эталонные кривые теперь ЕСТЬ: rendersnap2/ph*.npz (fir_re/fir_im).
### NEXT (24k+)
1. Дискретизация формулы: снять ph-серии с ОДИНАКОВЫм статичным контентом при
вариации одного параметра (q-серия уже есть рефами; прогнать rendersnap2 по
dual_b1q_0.1..10.0) → восстановить зависимость m(fir) от параметров.
2. Сравнить живой FIR с нашим structural mask: где именно расходится форма
(хвост? ширина? пол?) — починка detector-цепи по живым эталонам.
3. Проверить hannfall-мультипликацию: fit exp(0.984·log(bands))·w против
захваченного fir_im≠0 (фаза!) — complex ops A-D дают мнимость.
4. Починить wav-path баг rendersnap2 (проверяет rpp.wav вместо RENDER_FILE).
## ============ UPDATE 2026-08-24k (24k): ЗАКОН ПРИМЕНЕНИЯ НАЙДЕН ТОЧНО — A = 1.019·V^1.8345; «×1.805» = ПРОИЗВЕДЕНИЕ ЭКСПОНЕНТ 0.984×1.8345 ============
### Инструменты раунда (все рабочие, закоммичены)
- `scripts/rendersnap2.py` v3: мягкий снаппер (не убивает reaper), t_snap в npz,
слоты {540668 FIR, 540678 bands, 540688, 540788 R, 5407f8 R} + скалярный банк
0x540860..c0. OUT = argv[3] (иначе каталог перезаписывается!).
- `scripts/hunt2.py`: полный чанковый перебор ВСЕХ ctx-кандидатов в рендере.
РЕЗУЛЬТАТ: инстанс ОДИН (0x2370040) — GUI/DSP-пара (24c) НЕ ПОДТВЕРЖДЕНА,
гипотеза закрыта.
- Зондовый мультитон: 2 главных тона (= амплитуды dual.wav 0.4248) + зонды
−30 дБ на фикс. сетке; Гёрцель по рефу даёт применённый отклик на всех
зондовых частотах при том же адаптивном состоянии детектора.
### Ключевые отрицательные результаты (не возвращаться)
- [ctx+0x540888] и весь банк 0x540874..88c = 1.000000 весь рендер; 0x540870 =
440.955. Скаляров-множителей в применении НЕТ.
- R-слоты 0x540788/0x5407f8 ИДЕНТИЧНЫ во всех проектах (r43=4.0285/3.9807) —
статические twin-шаблоны; с формой V НЕ коррелируют (rms 0.47).
- Слот 0x540678 = спектр кадра (HF→0), не кривая маски; 0x540688 = большие
значения (~183) — сырые аккумуляторы.
- Свип для Y/X-отношения непригоден (энергия только на гребне) — только
мультитон/мультиуровень.
- Динамическое выравнивание V(t)↔g(t) на рампе НЕ сходится (инерция детектора
+ STOP-семплирование) — пары брать только из стационарных прогонов.
- Каталоги снапов перезаписываются друг другом — всегда отдельный argv[3].
### ГЛАВНЫЙ ЗАКОН (8 уровней драйва −0..−30 дБ, q=0.5, fc=500)
Стационарная пара (V = устоявшийся видимый FIR@540668 «most-common complete
state», A = применённый отклик по Гёрцелю рефа):
```
cutA[дБ] = 1.8345 · cutV[дБ] + 0.1615 дБ (@500 Гц, центр нотча)
rms = 0.0025 дБ (!), max 0.005 дБ — 8 точек, cutV от 1.0 до 5.55 дБ
```
На скирте (@2000): та же форма, γ≈1.812, offset≈0.18, rms 0.087 дБ.
В линейном домене: A ≈ 1.019 · V^1.8345.
Погрешность выбора V (медиана most-common вместо истинного кадра) ~0.05-0.1 дБ
на скирте объяснима; закон устойчив по всем уровням.
### РАЗГАДКА «×1.805» (23e/24i окончательно закрыты)
```
0.984 × 1.8345 = 1.80515
```
23e эмпирическая константа «свёртка ×1.805» — это ПРОИЗВЕДЕНИЕ экспонент:
видимый кернел строится как exp(0.984·log(bands)) (23d ✓), а ПРИМЕНЯЕМЫЙ
kerнел = bands^1.805 (в эквиваленте). Никакой свёртки/OLA/скаляра нет:
применение = побиновный комплекс-multiply (th_b3c0 ✓ 24e) на кернел
A = 1.019·V^1.8345. Старые phase*.npz (m43=0.588) — init-кернелы, всё что
строилось на них — артефакты.
### Двойные аттракторы видимого буфера
V_steady@43 = 0.524±0.002 ∀q (0.1..10); редкие флэши D@43=0.315 ∀q;
ln(D)/ln(V)=1.85 ≈ γ ⇒ флэш = применяемый кернел A, пойманный в буфере
(перестройка каждый кадр: rebuild→A→consume→запись V-стадии).
### Что осталось до транскрипции применения
1. Формула V(content,params): ширина провала vs q, глубина vs drive/sens,
позиция vs fc. Зондовая методика + rn_q*/rn_lv* каталоги = данные уже есть.
2. Источник 1.8345 в коде: искать вторую экспоненту в ops A-D / bigkernel
140b30 (exp2-кернел?) — кандидат: двойное применение стадии exp или
exp(0.984·log)²·коррекция окна (0.984·2=1.968≠1.805 — нет; искать точно).
3. RT_FIRCONV=3: маска = 1.019·M^1.8345, где M = выход нашей detector-цепи,
после того как M приведён к форме V (единичный passband, провалы у контента).
4. Корпусный гейт --vs-bridge после каждого шага.
### Контрольные числа уровня −0 дБ (dual-эквивалент)
V43=0.5279 → cutV=5.550 дБ; cutA=10.34 дБ (реф 10.32 ✓); cutA@2000=11.85 (реф
11.82 ✓). Уровневая серия: см. /tmp/opencode/{lv*,rn_lv*}, pairs в
level_pairs.npz.
### ДОПОЛНЕНИЕ 24k-2: чистая q-таблица V (most-common стеади, vfamily_clean.npz)
| q | m43 | m171 | примечание |
|-----|-------|-------|------------|
| 0.1 | 0.5240 | 0.3781 | |
| 0.2 | 0.5240 | 0.4099 | |
| 0.3 | 0.5240 | 0.4373 | |
| 0.5 | 0.5241 | 0.4728 | контроль 10.32/11.82 ✓ |
| 0.7 | 0.5241 | 0.4915 | |
| 1.5 | 0.5245 | 0.5141 | |
| 2.0 | 0.5250 | 0.5176 | |
| 3.0 | 0.5262 | 0.5203 | |
| 5.0 | 0.5298 | 0.5217 | |
| 10 | 0.5431 | 0.5223 | |
1. **Глубина в центре константна ∀q** (0.524±0.002, лёгкий подъём к q=10).
2. **Форма узкого провала ИНВАРИАНТНА по q**: ширина на полуглубине 6–7 бинов,
на 3/4 глубины 8–9 бинов ∀q; хвост +128 бинов = 1.0000 ∀q.
3. Вся q-зависимость — ШИРОКАЯ ФОНОВАЯ КОМПОНЕНТА: V(f;q) = D(f)·Wq(f),
где D — фиксированный узкий провал у контент-бина (форма от q не зависит),
Wq — широкая юбка с глубиной, растущей при q↓ (Wq@171: 0.72@q0.1 … 1.00@q10).
Проверка соответствия Wq статическому twin-шаблону R^−p — ПЕРВЫЙ кандидат
следующего раунда (данные: vfamily_clean.npz + слоты R788/R7f8).
4. Через степенной закон применения это даёт большие dB на скирте:
A@2000(q=0.1) = 1.019·0.378^1.8345 → 15.5 дБ vs реф 15.33 ✓.
5. Каталоги данных раунда: /tmp/opencode/rn_q* (q-серия), rn_lv* (уровни 0..30),
rn_ramp (рампа, для динамических опытов непригоден), multi2 (зонды+слоты),
level_pairs.npz, vfamily_clean.npz.
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""hunt2.py — enumerate ALL soothe2 module instances during offline render.
Chunk-correct full-heap scan (rendersnap-style) collecting EVERY ctx candidate
(vtable 0x1824AC210 or m48 marker), not just the first. Per candidate: sens,
fir43/fir171 (via ctx+0x540668 ptr), scalar bank snapshot. Goal: find the
GUI/DSP pair (24c/24j): visible instance holds shallow kernel while audio is
processed by another instance with the deep one.
"""
import hashlib
import os
import signal
import struct
import subprocess
import sys
import time
import numpy as np
VT = struct.pack('<Q', 0x1824AC210)
M48 = struct.pack('<I', 0x473b8000)
def find_host():
import glob
for p in glob.glob('/proc/[0-9]*'):
pid = int(os.path.basename(p))
try:
cmd = open(f'/proc/{pid}/cmdline', 'rb').read().replace(b'\0', b' ').decode('utf8', 'replace')
maps = open(f'/proc/{pid}/maps').read()
except Exception:
continue
if 'soothe2' in maps and 'reaper' not in cmd:
return pid
return None
def main():
rpp = sys.argv[1] if len(sys.argv) > 1 else '/tmp/opencode/multi.rpp'
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; "
"rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1", shell=True)
proc = subprocess.Popen(['/usr/bin/reaper', '-nosplash', '-ignoreerrors',
'-renderproject', rpp],
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
t0 = time.time()
host = None
while time.time() - t0 < 30 and not host:
host = find_host()
time.sleep(0.001)
if not host:
print('NO HOST')
return 1
fd = os.open(f'/proc/{host}/mem', os.O_RDONLY)
def rd(a, n):
try:
return os.pread(fd, n, a)
except OSError:
return None
def scan_all():
found = {}
for line in open(f'/proc/{host}/maps'):
parts = line.split()
if 'rw' not in parts[1]:
continue
lo, hi = (int(x, 16) for x in parts[0].split('-'))
CH = 16 * 1024 * 1024
a = lo
while a < hi:
d = rd(a, min(CH + 4096, hi - a))
if not d:
break
for pat, off in ((VT, 0), (M48, -0x24)):
j = d.find(pat)
while j >= 0:
cand = a + j + off
if cand not in found:
sb = rd(cand + 0x540870, 4)
if sb and struct.unpack('<f', sb)[0] > 100:
found[cand] = True
j = d.find(pat, j + 1)
a += CH
return list(found)
known = {}
rounds = 0
while time.time() - t0 < 25:
rounds += 1
try:
os.kill(host, signal.SIGSTOP)
except ProcessLookupError:
break
try:
cands = scan_all()
new = [c for c in cands if c not in known]
for c in new:
known[c] = rounds
pb = rd(c + 0x540668, 8)
m43 = m171 = -1
if pb:
p = struct.unpack('<Q', pb)[0]
if p > 0x10000:
fb = rd(p, 2049 * 8)
if fb:
arr = np.frombuffer(fb[:2049 * 8], dtype='<f4')
mag = np.hypot(arr[0::2], arr[1::2])
m43, m171 = float(mag[43]), float(mag[171])
sb = rd(c + 0x540888, 4)
s888 = struct.unpack('<f', sb)[0] if sb else -1
print('NEW ctx %#x @r%d t=%.2f fir43=%.4f fir171=%.4f s888=%.4f'
% (c, rounds, time.time() - t0, m43, m171, s888), flush=True)
# status of known ones every round
for c in known:
pb = rd(c + 0x540668, 8)
if pb:
p = struct.unpack('<Q', pb)[0]
if p > 0x10000:
fb = rd(p, 2049 * 8)
if fb:
arr = np.frombuffer(fb[:2049 * 8], dtype='<f4')
mag = np.hypot(arr[0::2], arr[1::2])
print(' st ctx %#x t=%.2f fir43=%.4f' % (c, time.time() - t0, mag[43]), flush=True)
finally:
try:
os.kill(host, signal.SIGCONT)
except ProcessLookupError:
pass
time.sleep(0.05)
print('total instances: %d' % len(known))
proc.kill()
return 0
if __name__ == '__main__':
sys.exit(main())
+195
View File
@@ -0,0 +1,195 @@
#!/usr/bin/env python3
"""rendersnap2.py — non-destructive variant of rendersnap.py.
Differences (24j):
- does NOT kill the host: lets -renderproject finish naturally so the output
wav is complete (partial-wav hazard, NOTES 24c);
- additionally dumps the ctx SCALAR bank 0x540860..0x5408c0 every phase
(goal: wet/FIR scalar [ctx+0x540888], BLOCKMAP 23b line 52b832);
- keeps sampling until host exit or phase cap, dedupe by FIR+scalar md5.
"""
import hashlib
import os
import signal
import struct
import subprocess
import sys
import time
import numpy as np
SLOTS = [0x540668, 0x540678, 0x540688, 0x540788, 0x5407f8]
NARR = 8194
SCAL_OFF = 0x540860
SCAL_N = 24 # floats -> 0x540860..0x5408c0
OUT = '/tmp/opencode/rendersnap2'
def find_host():
import glob
for p in glob.glob('/proc/[0-9]*'):
pid = int(os.path.basename(p))
try:
cmd = open(f'/proc/{pid}/cmdline', 'rb').read().replace(b'\0', b' ').decode('utf8', 'replace')
maps = open(f'/proc/{pid}/maps').read()
except Exception:
continue
if 'soothe2' in maps and 'reaper' not in cmd:
return pid
return None
def main():
rpp = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual_b1q_0.5.rpp'
cap = int(sys.argv[2]) if len(sys.argv) > 2 else 200
global OUT
OUT = sys.argv[3] if len(sys.argv) > 3 else OUT
os.makedirs(OUT, exist_ok=True)
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; "
"rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1", shell=True)
wav = rpp.replace('.rpp', '.wav')
if os.path.exists(wav):
os.remove(wav)
proc = subprocess.Popen(['/usr/bin/reaper', '-nosplash', '-ignoreerrors',
'-renderproject', rpp],
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
t0 = time.time()
host = None
while time.time() - t0 < 30 and not host:
host = find_host()
time.sleep(0.001)
if not host:
print('NO HOST')
return 1
print('host %d at %.3fs' % (host, time.time() - t0), flush=True)
fd = os.open(f'/proc/{host}/mem', os.O_RDONLY)
def rd(a, n):
try:
return os.pread(fd, n, a)
except OSError:
return None
vt = struct.pack('<Q', 0x1824AC210)
m48 = struct.pack('<I', 0x473b8000)
def scan_ctx():
for line in open(f'/proc/{host}/maps'):
parts = line.split()
if 'rw' not in parts[1]:
continue
lo, hi = (int(x, 16) for x in parts[0].split('-'))
CH = 16 * 1024 * 1024
a = lo
while a < hi:
d = rd(a, min(CH + 4096, hi - a))
if not d:
break
j = d.find(vt)
while j >= 0:
cand = a + j
sb = rd(cand + 0x540870, 4)
if sb and struct.unpack('<f', sb)[0] > 100:
return cand
j = d.find(vt, j + 1)
j = d.find(m48)
while j >= 0:
cand = a + j - 0x24
sb = rd(cand + 0x540870, 4)
if sb and struct.unpack('<f', sb)[0] > 100:
return cand
j = d.find(m48, j + 1)
a += CH
return None
ctx = None
while ctx is None and time.time() - t0 < 25:
try:
os.kill(host, signal.SIGSTOP)
except ProcessLookupError:
break
try:
ctx = scan_ctx()
finally:
if ctx is None:
try:
os.kill(host, signal.SIGCONT)
except ProcessLookupError:
break
if ctx is None:
time.sleep(0.004)
if not ctx:
print('NO CTX')
return 1
print('ctx %#x' % ctx, flush=True)
rng = np.random.default_rng(11)
prev = None
saved = 0
while True:
try:
os.kill(host, signal.SIGSTOP)
except ProcessLookupError:
print('host exited at %.2fs' % (time.time() - t0), flush=True)
break
try:
pb = rd(ctx + 0x540668, 8)
if not pb:
continue
p = struct.unpack('<Q', pb)[0]
fb = rd(p, NARR * 4)
if not fb:
continue
scal = rd(ctx + SCAL_OFF, SCAL_N * 4)
if scal is None:
continue
key = hashlib.md5(fb[:2049 * 8] + scal).digest()
if key != prev:
prev = key
arr = np.frombuffer(fb[:2049 * 8], dtype='<f4')
mag = np.hypot(arr[0::2], arr[1::2])
sv = np.frombuffer(scal, dtype='<f4')
store = {'fir_re': arr[0::2].copy(), 'fir_im': arr[1::2].copy(),
'scal': sv.copy()}
for off in SLOTS[1:]:
q = rd(ctx + off, 8)
if not q:
continue
ptr = struct.unpack('<Q', q)[0]
if ptr < 0x10000:
continue
ab = rd(ptr, NARR * 4)
if ab:
store[hex(off)] = np.frombuffer(ab, dtype='<f4').astype(np.float32)
np.savez_compressed(f'{OUT}/ph{saved:03d}.npz', t_snap=time.time() - t0,
**store)
print('PH%03d t=%.2f fir43=%.4f fir171=%.4f | s888=%.6f s88c=%.6f s874=%.6f s87c=%.6f s870=%.3f' %
(saved, time.time() - t0, mag[43], mag[171],
sv[10], sv[11], sv[5], sv[7], sv[4]), flush=True)
saved += 1
if saved >= cap:
break
finally:
try:
os.kill(host, signal.SIGCONT)
except ProcessLookupError:
pass
time.sleep(float(rng.uniform(0.0005, 0.006)))
try:
os.kill(host, 0)
except ProcessLookupError:
print('host exited at %.2fs' % (time.time() - t0), flush=True)
break
# wait for natural finish so wav completes
for _ in range(300):
if proc.poll() is not None:
break
time.sleep(0.1)
print('saved=%d reaper_rc=%s wav=%s' % (saved, proc.poll(),
os.path.getsize(wav) if os.path.exists(wav) else 'NONE'))
return 0
if __name__ == '__main__':
sys.exit(main())