Compare commits
2386
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a1c289ab2 | ||
|
|
bb380441a3 | ||
|
|
c9a3a1334b | ||
|
|
2b63c680b2 | ||
|
|
44a3b33c3a | ||
|
|
5d10a16baa | ||
|
|
33ccba1f3f | ||
|
|
bebc579180 | ||
|
|
a893f6b4f2 | ||
|
|
ecada765a2 | ||
|
|
b72e51accb | ||
|
|
ad0505bd31 | ||
|
|
64519c41a7 | ||
|
|
0d55c98add | ||
|
|
5549a69b1c | ||
|
|
1ceb0beb8d | ||
|
|
386ea85fea | ||
|
|
607de2a074 | ||
|
|
bf3834b95d | ||
|
|
3a00caef50 | ||
|
|
5505afd1f3 | ||
|
|
0e418c7fa9 | ||
|
|
f399ded5ba | ||
|
|
85d96fc857 | ||
|
|
3100985e4c | ||
|
|
6355bd1d73 | ||
|
|
3ed4b73ebf | ||
|
|
41cc04bdab | ||
|
|
d8621c7f27 | ||
|
|
3c49c2a4aa | ||
|
|
633f1d3044 | ||
|
|
34602e3e7e | ||
|
|
330e346a7b | ||
|
|
6d39800351 | ||
|
|
df2f90d1f5 | ||
|
|
1d6596dc61 | ||
|
|
0653121fc4 | ||
|
|
3f603534e9 | ||
|
|
631cc924cf | ||
|
|
9f1825dbf8 | ||
|
|
9163025912 | ||
|
|
5929bb4028 | ||
|
|
fe313cf8f8 | ||
|
|
cb9102f381 | ||
|
|
9a8bc9b461 | ||
|
|
c4bf292085 | ||
|
|
8d9704036f | ||
|
|
fb01a9bb2e | ||
|
|
1d7e12c562 | ||
|
|
7c473dd500 | ||
|
|
1ccb266971 | ||
|
|
7de54ab3e4 | ||
|
|
2c10272e15 | ||
|
|
55cc721c9b | ||
|
|
dea768c21a | ||
|
|
824c663ebd | ||
|
|
80b481827c | ||
|
|
8360e48622 | ||
|
|
41a68b86bc | ||
|
|
cfc0051088 | ||
|
|
cecc5d7d1f | ||
|
|
5e2e6c15cf | ||
|
|
d0c83cfff1 | ||
|
|
9d6d6454b0 | ||
|
|
94fa3207f6 | ||
|
|
5a837f903c | ||
|
|
00b4b92749 | ||
|
|
19a5870d5f | ||
|
|
bfa23afae9 | ||
|
|
909005c1a4 | ||
|
|
ddc163a2b7 | ||
|
|
ca471d777e | ||
|
|
2c4f107e63 | ||
|
|
3d0c4ff0a5 | ||
|
|
a9820bea42 | ||
|
|
52a73ea6a7 | ||
|
|
6e9bacd843 | ||
|
|
fbbd62acc4 | ||
|
|
ef673eb3b7 | ||
|
|
05b0927619 | ||
|
|
e3f2feb480 | ||
|
|
a65b0c8786 | ||
|
|
6c84e680af | ||
|
|
d6eed66926 | ||
|
|
88bf65e07d | ||
|
|
13c1831baa | ||
|
|
5609d57947 | ||
|
|
488be0edd1 | ||
|
|
e620567804 | ||
|
|
fdf2912427 | ||
|
|
51922cd18e | ||
|
|
79946bb07c | ||
|
|
41c32cf063 | ||
|
|
4f627bddff | ||
|
|
2a2a0ee267 | ||
|
|
5de0eb5115 | ||
|
|
cf6c3c99e8 | ||
|
|
c3a7f77b54 | ||
|
|
4f5651906c | ||
|
|
2df21efc22 | ||
|
|
b0f5055c31 | ||
|
|
80eb331d6a | ||
|
|
bb87a0845b | ||
|
|
f7c8b95af6 | ||
|
|
33130c1d74 | ||
|
|
f61c0c3e38 | ||
|
|
2410c39b71 | ||
|
|
5a57b59b50 | ||
|
|
ff3eb02bff | ||
|
|
56d5fcd75a | ||
|
|
484b620016 | ||
|
|
16f81575ce | ||
|
|
10b039686c | ||
|
|
194bf7bb45 | ||
|
|
a288afd8ce | ||
|
|
4f3f6d3e60 | ||
|
|
1c4010ebed | ||
|
|
ace9cb70d0 | ||
|
|
af3c7a4217 | ||
|
|
e403a2f689 | ||
|
|
3944459c29 | ||
|
|
48383ef95e | ||
|
|
f020a6f62b | ||
|
|
d23a96a08e | ||
|
|
dffcd19d21 | ||
|
|
9079ab5c24 | ||
|
|
7e39f16e0f | ||
|
|
00819654cf | ||
|
|
346c06f31d | ||
|
|
ba7c2873c5 | ||
|
|
0e15234ce1 | ||
|
|
e5fafacf99 | ||
|
|
d467d3aa8f | ||
|
|
d5e489598d | ||
|
|
1ff02188ca | ||
|
|
f31b34d0f0 | ||
|
|
ecd5b9fc9d | ||
|
|
aca6364378 | ||
|
|
88e930cc0a | ||
|
|
be235e01aa | ||
|
|
66f004f725 | ||
|
|
cbe22899c2 | ||
|
|
df36e504fc | ||
|
|
7c814f5cb9 | ||
|
|
9ba78c084b | ||
|
|
d644126cac | ||
|
|
f59f64ea48 | ||
|
|
f5f106c682 | ||
|
|
abf342534f | ||
|
|
8e46cf9254 | ||
|
|
15b037aef7 | ||
|
|
69e534304b | ||
|
|
4dbc543cd3 | ||
|
|
4f6808156f | ||
|
|
5870efdf04 | ||
|
|
34db81c5cf | ||
|
|
a31436e421 | ||
|
|
550f384c69 | ||
|
|
a6b909b70e | ||
|
|
fcb0768052 | ||
|
|
5d9da0c6b1 | ||
|
|
e8ee608ba9 | ||
|
|
4e5530aca0 | ||
|
|
4fa3b18cc2 | ||
|
|
50cf61e9cd | ||
|
|
c6ed01110e | ||
|
|
1764603977 | ||
|
|
6c619d5f2a | ||
|
|
58c0fc6bba | ||
|
|
32e648ea9e | ||
|
|
b2efab9806 | ||
|
|
a342316e7a | ||
|
|
c887780022 | ||
|
|
a538f99bf0 | ||
|
|
9fb4823df0 | ||
|
|
a2fb562a22 | ||
|
|
852c05590d | ||
|
|
a54031081b | ||
|
|
23c4e4efbc | ||
|
|
5bc0c1ece4 | ||
|
|
fbab6eae5d | ||
|
|
0d9c728efe | ||
|
|
039c4a9ed4 | ||
|
|
179f248db8 | ||
|
|
589cdcc81f | ||
|
|
6997a82af1 | ||
|
|
c6f7d50838 | ||
|
|
7ce60ca0f2 | ||
|
|
b1112981fa | ||
|
|
a4419b4c89 | ||
|
|
1b036e4ea9 | ||
|
|
f38aa3b08d | ||
|
|
c92d36354a | ||
|
|
a0d49c4bbd | ||
|
|
e3dbeee6a8 | ||
|
|
8673673fcb | ||
|
|
03e8caf849 | ||
|
|
2781208361 | ||
|
|
64064c2cbf | ||
|
|
e5510e9f62 | ||
|
|
44db42180e | ||
|
|
6dab5db56a | ||
|
|
f1cccb1c92 | ||
|
|
b26538c48f | ||
|
|
39bca58515 | ||
|
|
ca7e169811 | ||
|
|
fe9c3c74bd | ||
|
|
d7053a2366 | ||
|
|
59858a726a | ||
|
|
c4da659dcb | ||
|
|
5b6ed6ee78 | ||
|
|
7e95c3043b | ||
|
|
352f5b3f94 | ||
|
|
6c74749dd4 | ||
|
|
5c1619420a | ||
|
|
c4b2052b27 | ||
|
|
b46f13920c | ||
|
|
d7d018d591 | ||
|
|
ba26b14f16 | ||
|
|
ea5d1fcd45 | ||
|
|
68fb937063 | ||
|
|
03f7d4341d | ||
|
|
b3ebc09b9b | ||
|
|
c98fdc6297 | ||
|
|
b3116173ca | ||
|
|
4a44149c00 | ||
|
|
5f0325e817 | ||
|
|
9e5b2ad435 | ||
|
|
14b152bb5a | ||
|
|
75806249bd | ||
|
|
4e386fc8e3 | ||
|
|
57d28376b6 | ||
|
|
c8bcf41f4a | ||
|
|
fd05ec47ab | ||
|
|
bad9ee3a0c | ||
|
|
05deb15c91 | ||
|
|
1b49c4023b | ||
|
|
103e0b6865 | ||
|
|
a0b6dd7ef5 | ||
|
|
94697c1af0 | ||
|
|
4fa9d474ab | ||
|
|
39446b6e2c | ||
|
|
0bc3b62f46 | ||
|
|
9c7b49e91f | ||
|
|
f87cda8b13 | ||
|
|
b250e8a44d | ||
|
|
961c3447d0 | ||
|
|
e59bc9857b | ||
|
|
4a1a65c3ba | ||
|
|
e416d69212 | ||
|
|
8d5459f8fa | ||
|
|
5aa28a11d6 | ||
|
|
b02ad6f4cf | ||
|
|
de6712de91 | ||
|
|
1e49d2554c | ||
|
|
d2f0828917 | ||
|
|
390747343b | ||
|
|
475bec52ca | ||
|
|
4db89eeb32 | ||
|
|
36e8ab228c | ||
|
|
91478e2593 | ||
|
|
e3b74de7ef | ||
|
|
0cd1955606 | ||
|
|
8ad5c09474 | ||
|
|
7ae204ba4f | ||
|
|
a4b9367344 | ||
|
|
74313498f2 | ||
|
|
d71d0dc4af | ||
|
|
8b438f26a5 | ||
|
|
bea8f1f901 | ||
|
|
dd5dee2767 | ||
|
|
3eccf58c5e | ||
|
|
65f7babb2a | ||
|
|
2c828d481d | ||
|
|
49317aab75 | ||
|
|
3a7aab9186 | ||
|
|
521dce5528 | ||
|
|
3037c5fa87 | ||
|
|
52d9a347c3 | ||
|
|
8c826fa2e7 | ||
|
|
3db52c755c | ||
|
|
9bc3e7228c | ||
|
|
0a73d140e6 | ||
|
|
d8d9f7c4a4 | ||
|
|
27cbf2ba44 | ||
|
|
4a759115c9 | ||
|
|
d991b4c63e | ||
|
|
238ebcc751 | ||
|
|
0830fe4461 | ||
|
|
f6701f81f9 | ||
|
|
0d08c21330 | ||
|
|
4f1af6456d | ||
|
|
a852a9738b | ||
|
|
ee1e28bff9 | ||
|
|
74a4135cdc | ||
|
|
9d6dc1b469 | ||
|
|
c0d5db318f | ||
|
|
7be4b8884c | ||
|
|
08c74894ef | ||
|
|
f2d67990f8 | ||
|
|
7122d70ea6 | ||
|
|
5c1807a712 | ||
|
|
9b5743123b | ||
|
|
833eeb0a8d | ||
|
|
72cb1fa829 | ||
|
|
7b341d9f4f | ||
|
|
c211e17af9 | ||
|
|
fcdc098415 | ||
|
|
6e0d3f3022 | ||
|
|
cf2fc67a53 | ||
|
|
902e7bb921 | ||
|
|
bf8dfeec02 | ||
|
|
720ee64fd1 | ||
|
|
fb660f2b3e | ||
|
|
f50f717b35 | ||
|
|
7a0bc9516c | ||
|
|
eb11d2ebc5 | ||
|
|
9a3ef7b622 | ||
|
|
51c6bbb96f | ||
|
|
c3c88457a8 | ||
|
|
55e87d20bf | ||
|
|
9913222e03 | ||
|
|
8c22df8f8e | ||
|
|
d0232e4c6e | ||
|
|
6f542bfae9 | ||
|
|
d9843b8cbd | ||
|
|
93f6fa1d52 | ||
|
|
402c0c0611 | ||
|
|
9ca0e61361 | ||
|
|
562bdf54a6 | ||
|
|
193a549d47 | ||
|
|
7b38fd0269 | ||
|
|
10018d008e | ||
|
|
ae1d05deda | ||
|
|
64761fd945 | ||
|
|
5e38e5b71d | ||
|
|
9d45488369 | ||
|
|
16bdf95ba3 | ||
|
|
2a08fd8acd | ||
|
|
ecdba8fb63 | ||
|
|
243233a20b | ||
|
|
a4a687be4c | ||
|
|
f9348ce36f | ||
|
|
5f45b38cfb | ||
|
|
8eb869e41c | ||
|
|
4bb2e42a7e | ||
|
|
895cdd5ac2 | ||
|
|
ecae1cc3c2 | ||
|
|
713a972c01 | ||
|
|
dc22014bcc | ||
|
|
28f3b264b8 | ||
|
|
baeb4973dd | ||
|
|
c92f7991ec | ||
|
|
40dcc76adb | ||
|
|
47033718ad | ||
|
|
526ee5ec8b | ||
|
|
9bffe49e62 | ||
|
|
642fc34bcc | ||
|
|
12e9e4da37 | ||
|
|
d266c5da25 | ||
|
|
0f22be6885 | ||
|
|
b7ae7ef700 | ||
|
|
a9b91145a6 | ||
|
|
4ed2c4bbab | ||
|
|
5f1985f9ab | ||
|
|
943f9f2646 | ||
|
|
02e7bbd9af | ||
|
|
b11a1beb44 | ||
|
|
322947777c | ||
|
|
c0422a73c0 | ||
|
|
0e53a9f5dd | ||
|
|
486d3314ef | ||
|
|
13f09cfc20 | ||
|
|
5f772fa97f | ||
|
|
0bc2715a30 | ||
|
|
0a2700a387 | ||
|
|
4e9cce8a01 | ||
|
|
0eb743d627 | ||
|
|
284a1268e4 | ||
|
|
7cbe5375d4 | ||
|
|
02a11f7d0a | ||
|
|
21a20da5ee | ||
|
|
39ed42da2e | ||
|
|
94e79e5630 | ||
|
|
a1f8eaeec2 | ||
|
|
187fc7fee8 | ||
|
|
bd2dd056d6 | ||
|
|
4cebd0be54 | ||
|
|
7d5399cd62 | ||
|
|
97b537c005 | ||
|
|
558b2a2ac0 | ||
|
|
a9d63480cf | ||
|
|
27b0f64ec0 | ||
|
|
cda46c0b82 | ||
|
|
4deee21d9f | ||
|
|
5a318f6f60 | ||
|
|
babcb7786a | ||
|
|
b21aeaa6f8 | ||
|
|
79b4235960 | ||
|
|
4683c98c08 | ||
|
|
a72668a491 | ||
|
|
03a4462407 | ||
|
|
a2fdbb6afb | ||
|
|
d4cd80d3dd | ||
|
|
37cf6089e7 | ||
|
|
b68116eedf | ||
|
|
6a4a0f45d8 | ||
|
|
b7aaf268af | ||
|
|
be367a08b6 | ||
|
|
6ad9c8c80b | ||
|
|
7ed1a9b167 | ||
|
|
505185430c | ||
|
|
1d28ced4f4 | ||
|
|
fdd39e82e2 | ||
|
|
aca14bfb70 | ||
|
|
7693db7b4d | ||
|
|
30f79f7a8d | ||
|
|
cf4edbbf62 | ||
|
|
9712a0a583 | ||
|
|
cd58903101 | ||
|
|
a8c2ce8401 | ||
|
|
dad819282a | ||
|
|
04acb6f836 | ||
|
|
c0626e0387 | ||
|
|
e61dc8cd06 | ||
|
|
a0e440c0f5 | ||
|
|
1b3269f76b | ||
|
|
613a75becc | ||
|
|
5f9e9975d3 | ||
|
|
a73d829080 | ||
|
|
a831cf6295 | ||
|
|
b57c8e5041 | ||
|
|
25376197db | ||
|
|
62a976aef5 | ||
|
|
9bd9f04bcf | ||
|
|
949c300c53 | ||
|
|
23ece16940 | ||
|
|
dbb07df7e9 | ||
|
|
b48a91853d | ||
|
|
34890ea709 | ||
|
|
a97c915663 | ||
|
|
3f97dafbf7 | ||
|
|
427794fed8 | ||
|
|
858ba38caf | ||
|
|
310d997af3 | ||
|
|
849f1c8618 | ||
|
|
b0fe2d66af | ||
|
|
7f4ac2fadf | ||
|
|
639be7fcc2 | ||
|
|
a84b226b18 | ||
|
|
214498f892 | ||
|
|
62633bc924 | ||
|
|
ffe563bb9f | ||
|
|
bdb6bf80a4 | ||
|
|
d0bbe8444e | ||
|
|
1084cd10ec | ||
|
|
ec545a8665 | ||
|
|
d7f30d7bb1 | ||
|
|
50017b7f74 | ||
|
|
7140775d8d | ||
|
|
5568331510 | ||
|
|
242d0f5493 | ||
|
|
6e0b842858 | ||
|
|
2aa890201d | ||
|
|
aff8a21ae4 | ||
|
|
fae794d39e | ||
|
|
fe86a16cac | ||
|
|
00698537ad | ||
|
|
df02aa8170 | ||
|
|
0070221045 | ||
|
|
54b50f4177 | ||
|
|
a51af91718 | ||
|
|
bd92ac0c88 | ||
|
|
4b88402f84 | ||
|
|
0cfc29d9eb | ||
|
|
d1ba26f8ac | ||
|
|
6d5a626b9b | ||
|
|
f7853f145c | ||
|
|
78b147f806 | ||
|
|
b8bc9b409a | ||
|
|
e246357f92 | ||
|
|
aedb0e6170 | ||
|
|
f8827a9466 | ||
|
|
0730fb6bdd | ||
|
|
5e672f01d9 | ||
|
|
66526ad4a5 | ||
|
|
86a278ee99 | ||
|
|
0343825746 | ||
|
|
09f48403c5 | ||
|
|
2facdb6949 | ||
|
|
ae8eeab490 | ||
|
|
6715acdef8 | ||
|
|
3ed9acb384 | ||
|
|
21e4821e22 | ||
|
|
f6051d3bcd | ||
|
|
5a55bcb500 | ||
|
|
03ff431d78 | ||
|
|
4c62f45984 | ||
|
|
80b23997c0 | ||
|
|
f6f8647091 | ||
|
|
0e2253cdc8 | ||
|
|
044a20d4a0 | ||
|
|
b40ba9308f | ||
|
|
74e2238b7f | ||
|
|
81ee235f73 | ||
|
|
dcdc700d8d | ||
|
|
81de8747e2 | ||
|
|
28826e050c | ||
|
|
aad295c9d0 | ||
|
|
b3495349e1 | ||
|
|
7e08135bba | ||
|
|
057bdb3112 | ||
|
|
a31ade1054 | ||
|
|
98f71735a0 | ||
|
|
1896a19eba | ||
|
|
8a46a53a21 | ||
|
|
5d2522b8de | ||
|
|
feb327afc7 | ||
|
|
3c395dfe5d | ||
|
|
4844253f12 | ||
|
|
01e1772615 | ||
|
|
fd61aedce3 | ||
|
|
9f36fb0ddd | ||
|
|
de1da1c9b1 | ||
|
|
3f48a145ae | ||
|
|
6c8e2763e5 | ||
|
|
945208ce77 | ||
|
|
62a4f60508 | ||
|
|
ada1406d79 | ||
|
|
ade9401e05 | ||
|
|
ad7050ad2c | ||
|
|
7cffb15c73 | ||
|
|
2b7def369d | ||
|
|
9f813424bb | ||
|
|
4703a46eb9 | ||
|
|
61d1561612 | ||
|
|
41696b7e0b | ||
|
|
e5a1fc5a48 | ||
|
|
60e040677b | ||
|
|
321ed8e747 | ||
|
|
e45401449e | ||
|
|
6a90d79d04 | ||
|
|
b4ef4ec919 | ||
|
|
9c22b04cb0 | ||
|
|
d6db3d18ce | ||
|
|
be295f768c | ||
|
|
97710b609f | ||
|
|
75d383f89d | ||
|
|
a20a219063 | ||
|
|
6075b4cd0f | ||
|
|
49385ab91c | ||
|
|
094a8bb499 | ||
|
|
df2f6ff300 | ||
|
|
37e092672a | ||
|
|
7bf877b7d6 | ||
|
|
305b61ccab | ||
|
|
1d1f5d081f | ||
|
|
edc78cbf4a | ||
|
|
a72e514dc5 | ||
|
|
6ad3674904 | ||
|
|
7c7b9646f3 | ||
|
|
575a7bfe32 | ||
|
|
cb8e654639 | ||
|
|
3f7cfe1c86 | ||
|
|
a485b10e63 | ||
|
|
3e29a7ca1c | ||
|
|
6cbcb860d6 | ||
|
|
86c08092af | ||
|
|
1dfe349eb8 | ||
|
|
c69f8d8975 | ||
|
|
60104ea093 | ||
|
|
2513e9ae81 | ||
|
|
d2a8e6fda7 | ||
|
|
2c7c3c9e8b | ||
|
|
857092508a | ||
|
|
b588f8e9f4 | ||
|
|
62aee9f01c | ||
|
|
c42d75c2f5 | ||
|
|
5c0db6ce9e | ||
|
|
6363c8b7ea | ||
|
|
5eb3526186 | ||
|
|
95f209d098 | ||
|
|
d7ad58b380 | ||
|
|
dbe2640e74 | ||
|
|
7a40ba536e | ||
|
|
8983026625 | ||
|
|
f5d923a671 | ||
|
|
417b848b50 | ||
|
|
88ac08333e | ||
|
|
aede36388f | ||
|
|
906f247588 | ||
|
|
fad9fbe6c3 | ||
|
|
b539a6f338 | ||
|
|
58755dd75e | ||
|
|
bbb8cff4a6 | ||
|
|
3c321ac59c | ||
|
|
eb09a675c7 | ||
|
|
4284233e6f | ||
|
|
5a8b05c50f | ||
|
|
b9a5981f5c | ||
|
|
78f71ddbb0 | ||
|
|
37461bb938 | ||
|
|
71abf74bfc | ||
|
|
79d8789410 | ||
|
|
6037819269 | ||
|
|
b96ae1a718 | ||
|
|
6a2164717e | ||
|
|
eb3dd7f237 | ||
|
|
d70d6cb66a | ||
|
|
83efa229ed | ||
|
|
308eb07742 | ||
|
|
d5715a1f4f | ||
|
|
27a87412c5 | ||
|
|
7673316dc8 | ||
|
|
4fab849917 | ||
|
|
38447da5e8 | ||
|
|
8158d6a435 | ||
|
|
3cb971fcba | ||
|
|
110dd82932 | ||
|
|
26ab3b13b4 | ||
|
|
df80c5745d | ||
|
|
1dc26bf715 | ||
|
|
93dc21f3c8 | ||
|
|
fd73fae77f | ||
|
|
9c7d088f8b | ||
|
|
64bbbdc631 | ||
|
|
f81b93ad0d | ||
|
|
88e3e27145 | ||
|
|
bf729502a1 | ||
|
|
f5660a4642 | ||
|
|
e440c62339 | ||
|
|
4a6f95cebd | ||
|
|
49d55a25a1 | ||
|
|
1e21912dc6 | ||
|
|
a63af43e59 | ||
|
|
98d0040b27 | ||
|
|
2a439754ba | ||
|
|
4401c4b1e4 | ||
|
|
8cb656dbdc | ||
|
|
eb98d2c724 | ||
|
|
f90cbae94a | ||
|
|
5116496565 | ||
|
|
7a2fa9e199 | ||
|
|
5c9bf9e47c | ||
|
|
8d7ab6a1a6 | ||
|
|
97e20d2f1c | ||
|
|
c27ee36cfb | ||
|
|
37eccd06ea | ||
|
|
16e547d15d | ||
|
|
114c0b3e2f | ||
|
|
688d559787 | ||
|
|
0f5a5580c5 | ||
|
|
85101765eb | ||
|
|
fb2a615165 | ||
|
|
c03fe3da71 | ||
|
|
0c26daf3f5 | ||
|
|
b463fc3309 | ||
|
|
e37b3be7da | ||
|
|
ca09073a52 | ||
|
|
efe9e8311c | ||
|
|
3c73c7d645 | ||
|
|
0b58bf3c87 | ||
|
|
360d5dddaa | ||
|
|
38eae06161 | ||
|
|
c37be1c3e8 | ||
|
|
014a110fd5 | ||
|
|
69cee1bec4 | ||
|
|
2de9ac85a2 | ||
|
|
cb84d1049f | ||
|
|
91357ebf90 | ||
|
|
110af9b0c5 | ||
|
|
432f2dcf00 | ||
|
|
26f6e2ee83 | ||
|
|
48cb101047 | ||
|
|
05f1fe485c | ||
|
|
349869f732 | ||
|
|
230687d155 | ||
|
|
de71617e9f | ||
|
|
543c9decba | ||
|
|
9629e9311c | ||
|
|
21f4de492c | ||
|
|
7a660688af | ||
|
|
9500581e0b | ||
|
|
3ba7601ba4 | ||
|
|
86da11b2e1 | ||
|
|
903568d9d6 | ||
|
|
debfccd7fa | ||
|
|
91620cec4e | ||
|
|
5074bad57c | ||
|
|
0475df8412 | ||
|
|
7454bebb14 | ||
|
|
ea971b0856 | ||
|
|
5725cfb686 | ||
|
|
3228cce0fb | ||
|
|
44c9c3bcaf | ||
|
|
ce0fa1f063 | ||
|
|
cf3c8c7c34 | ||
|
|
ae1682bbe2 | ||
|
|
7c1955f029 | ||
|
|
5753177f29 | ||
|
|
e24ab45725 | ||
|
|
d60f8e4b1b | ||
|
|
198ebc0acb | ||
|
|
00e9ed227a | ||
|
|
ac8e701406 | ||
|
|
3d2f90e8ae | ||
|
|
218249f9a5 | ||
|
|
9bacde3bc1 | ||
|
|
6228873ce2 | ||
|
|
ea46c56bc1 | ||
|
|
bb5759728a | ||
|
|
fc526d16c0 | ||
|
|
919c3d76fe | ||
|
|
700034eec8 | ||
|
|
6cdc2ce4dc | ||
|
|
8f71a1161b | ||
|
|
7e7d86e4d3 | ||
|
|
e3fb8377ab | ||
|
|
c4fc4e9b1e | ||
|
|
25d27bfffe | ||
|
|
559d66ae20 | ||
|
|
2a572af2bb | ||
|
|
e47ff579b3 | ||
|
|
f4d4b6033e | ||
|
|
c9c1bb7cf1 | ||
|
|
db7264e271 | ||
|
|
d14f9233ed | ||
|
|
0f2e81bd0f | ||
|
|
d766a50447 | ||
|
|
829e4d41fa | ||
|
|
b523282e8e | ||
|
|
2038e33d8d | ||
|
|
051d453c2d | ||
|
|
9d69cce8e8 | ||
|
|
4a49c830d1 | ||
|
|
d4eb1da233 | ||
|
|
95942382bc | ||
|
|
05ed516cf5 | ||
|
|
6d7b3e8e02 | ||
|
|
7c5b76b482 | ||
|
|
abdca1acc7 | ||
|
|
3e4d5d235f | ||
|
|
0b136ae2ab | ||
|
|
b88d76a8ab | ||
|
|
7477da42a5 | ||
|
|
3276204196 | ||
|
|
612fb7816d | ||
|
|
9886bc2fde | ||
|
|
ff51e30a55 | ||
|
|
a373180c10 | ||
|
|
717d965fec | ||
|
|
0d65c5bb74 | ||
|
|
b211da9a04 | ||
|
|
5255133f27 | ||
|
|
eb43876bb6 | ||
|
|
408c0be021 | ||
|
|
7f9eef8579 | ||
|
|
2ff14519fa | ||
|
|
24ba454821 | ||
|
|
d7788b87ac | ||
|
|
d1dae1ac2a | ||
|
|
e2102bbbcc | ||
|
|
19a135d552 | ||
|
|
23c8a190dd | ||
|
|
29b01237aa | ||
|
|
8add40ec00 | ||
|
|
a165372bbe | ||
|
|
3226865ad7 | ||
|
|
075ffc4d3b | ||
|
|
a3854f2836 | ||
|
|
f836ba67ce | ||
|
|
0a56c01e29 | ||
|
|
ede911e7e4 | ||
|
|
7a6ecf700c | ||
|
|
9d422dd798 | ||
|
|
43522936ff | ||
|
|
e68df70aad | ||
|
|
11a3a20655 | ||
|
|
9d948d399e | ||
|
|
54e672420c | ||
|
|
1a97219c43 | ||
|
|
b3ed8b06e7 | ||
|
|
9c71115363 | ||
|
|
e1bb12d16a | ||
|
|
3083cc05dc | ||
|
|
cfa3ed1f61 | ||
|
|
cb9852e8d5 | ||
|
|
b6b0726bfe | ||
|
|
c244808b77 | ||
|
|
b7b043777b | ||
|
|
26c91e6295 | ||
|
|
6896038f30 | ||
|
|
22a03c5880 | ||
|
|
c1cc3c459b | ||
|
|
aaff1a5494 | ||
|
|
5f9262dd9a | ||
|
|
3f13860eac | ||
|
|
962067c2ac | ||
|
|
ff16adda17 | ||
|
|
f1aec9bb2d | ||
|
|
39c5274819 | ||
|
|
92f62a0121 | ||
|
|
e6954b4152 | ||
|
|
3f41676a3d | ||
|
|
59fb020711 | ||
|
|
1b07a8a708 | ||
|
|
7d75afd4c5 | ||
|
|
233056a83a | ||
|
|
3c750d018c | ||
|
|
4cc7d5eaf4 | ||
|
|
4476e875ab | ||
|
|
dba813e8c5 | ||
|
|
96725caf03 | ||
|
|
378d4103a8 | ||
|
|
91030de5ea | ||
|
|
50bcf5539a | ||
|
|
cbc7b296bb | ||
|
|
92a942ea8b | ||
|
|
80f6997e1b | ||
|
|
6765cf186d | ||
|
|
d944418676 | ||
|
|
1da19276e5 | ||
|
|
a7c67ee488 | ||
|
|
9c4219d837 | ||
|
|
a439b222da | ||
|
|
a82c2ede6a | ||
|
|
b51e2c0817 | ||
|
|
ac2029c82b | ||
|
|
c5efa56939 | ||
|
|
b4092d1bd6 | ||
|
|
39dbfa7673 | ||
|
|
f341d1db4b | ||
|
|
e53ea2e04a | ||
|
|
d1cbb64884 | ||
|
|
4c2d8aa4cc | ||
|
|
ac3801761e | ||
|
|
15e4fcb710 | ||
|
|
4447f25e37 | ||
|
|
f84b24806e | ||
|
|
459b1ecc01 | ||
|
|
55293483da | ||
|
|
fc0559c974 | ||
|
|
f8e960c844 | ||
|
|
5666da8859 | ||
|
|
0c214dfca0 | ||
|
|
78246e249e | ||
|
|
fe2ccb07e1 | ||
|
|
ee9af6533d | ||
|
|
f3eed42c3f | ||
|
|
209f2ceb7c | ||
|
|
95ab911caf | ||
|
|
b665191b9e | ||
|
|
572392511b | ||
|
|
4f91f575a1 | ||
|
|
5cd2df9125 | ||
|
|
964c17f1bf | ||
|
|
64f1fe4dfb | ||
|
|
129f809b7b | ||
|
|
e7ff764ed0 | ||
|
|
f61e1c0a9f | ||
|
|
1b3d9ff8ee | ||
|
|
97e3b3e9ef | ||
|
|
a58cfcedc6 | ||
|
|
170a6b0d57 | ||
|
|
9f8fb64e6e | ||
|
|
401ca96d7d | ||
|
|
e1b29af8ce | ||
|
|
1376f9f059 | ||
|
|
eeb3ca5927 | ||
|
|
5e22da3098 | ||
|
|
13feaca674 | ||
|
|
607952b709 | ||
|
|
1b0b429835 | ||
|
|
0dec628e49 | ||
|
|
152d89f0dc | ||
|
|
8582d2e2b0 | ||
|
|
5c6f71c602 | ||
|
|
6ff21c12ef | ||
|
|
b1390a9542 | ||
|
|
1c5ef12c8f | ||
|
|
b6961f5fbc | ||
|
|
2db038beee | ||
|
|
898154b2b6 | ||
|
|
21c22bd911 | ||
|
|
a0db643acb | ||
|
|
87a2075dec | ||
|
|
434b59b1c7 | ||
|
|
4a7d64b2aa | ||
|
|
21ab48b6c3 | ||
|
|
ef397a6b48 | ||
|
|
faee775dff | ||
|
|
8b4b278f3c | ||
|
|
61bcf28720 | ||
|
|
5d115c3dac | ||
|
|
6910e07c31 | ||
|
|
5f16c79559 | ||
|
|
047927a7a5 | ||
|
|
005d891fb4 | ||
|
|
634ef222a4 | ||
|
|
f47b7167a2 | ||
|
|
8703e2678a | ||
|
|
afe9437c49 | ||
|
|
d32c8ed6e3 | ||
|
|
31a0c4f1a3 | ||
|
|
aad77cf415 | ||
|
|
ea5ef4abc2 | ||
|
|
96e8f6c0ac | ||
|
|
2f1de49e87 | ||
|
|
fe16509293 | ||
|
|
47cca28d39 | ||
|
|
4eece00a2d | ||
|
|
5e15fc0d3f | ||
|
|
68e5905d9e | ||
|
|
3063270a67 | ||
|
|
c7184b55d8 | ||
|
|
ce2560ef4b | ||
|
|
53c6437301 | ||
|
|
520f7a29a7 | ||
|
|
a06bbc34df | ||
|
|
b2c6500fea | ||
|
|
b01d54a431 | ||
|
|
156b250432 | ||
|
|
9becdac331 | ||
|
|
ba2a66341a | ||
|
|
a59ebcd6db | ||
|
|
04f4d29e4f | ||
|
|
edf7e4d2c5 | ||
|
|
5dbc5bcf59 | ||
|
|
c986221cb6 | ||
|
|
d01bc395fa | ||
|
|
af35d03399 | ||
|
|
e682f30ca9 | ||
|
|
cabae34791 | ||
|
|
b091aa0df2 | ||
|
|
57a59df983 | ||
|
|
5e37aaa2fb | ||
|
|
65d7ec875e | ||
|
|
78d3511174 | ||
|
|
f055f8a9df | ||
|
|
8c12dfc02f | ||
|
|
ea65d87ec1 | ||
|
|
3f61549769 | ||
|
|
534f319d86 | ||
|
|
fa53139163 | ||
|
|
d4e6bc5980 | ||
|
|
2a22d625e1 | ||
|
|
cab992228c | ||
|
|
0f570edbd0 | ||
|
|
327478475c | ||
|
|
3b3ba3c495 | ||
|
|
43a90ff23b | ||
|
|
45720ebaba | ||
|
|
dca1adc173 | ||
|
|
f89494fb4f | ||
|
|
abf7ccf566 | ||
|
|
e20f4113a4 | ||
|
|
5d8c6b8ff3 | ||
|
|
4b414deb6e | ||
|
|
a6b8594219 | ||
|
|
6980bc7a88 | ||
|
|
cc341221d4 | ||
|
|
f04e3fd995 | ||
|
|
6cbb13d11d | ||
|
|
0fe9a779f0 | ||
|
|
03d8f71be4 | ||
|
|
4abc5ef6e7 | ||
|
|
5789a83ea2 | ||
|
|
6b409a5006 | ||
|
|
ed848f94eb | ||
|
|
23bd954b79 | ||
|
|
2c9904c345 | ||
|
|
996d4a89f5 | ||
|
|
b466ec30e3 | ||
|
|
609221e384 | ||
|
|
14a56d9b33 | ||
|
|
dffde9691d | ||
|
|
b00c6a2b6d | ||
|
|
c8bbce6a94 | ||
|
|
5fbea21482 | ||
|
|
120a865152 | ||
|
|
610773fa90 | ||
|
|
4a55c30f95 | ||
|
|
a978bb7b06 | ||
|
|
e975d24a73 | ||
|
|
023f3a335e | ||
|
|
9fe5cd4c27 | ||
|
|
c041882f5f | ||
|
|
0195732b4e | ||
|
|
2f3eb66c4c | ||
|
|
39bfedca86 | ||
|
|
7bac999d93 | ||
|
|
9aba8e278c | ||
|
|
ab283eebdc | ||
|
|
96e7983128 | ||
|
|
dc3cf4f227 | ||
|
|
2e1908a52b | ||
|
|
ab04b14237 | ||
|
|
fe2fa73cc0 | ||
|
|
991d6bad43 | ||
|
|
b5bd45c4da | ||
|
|
4939ee7fb0 | ||
|
|
811b980883 | ||
|
|
1d43578335 | ||
|
|
edcd34d942 | ||
|
|
ceb06bafcf | ||
|
|
183e66e97d | ||
|
|
d441d9634d | ||
|
|
05d9e3f22e | ||
|
|
801050bacb | ||
|
|
f24a8ca6d3 | ||
|
|
0bbd4d6c34 | ||
|
|
6a1e7a90dd | ||
|
|
859bb733af | ||
|
|
f87c6a93bc | ||
|
|
61faca84d7 | ||
|
|
0de5479e8e | ||
|
|
8eef506eed | ||
|
|
c39610f079 | ||
|
|
7ff95623c1 | ||
|
|
931631a4a9 | ||
|
|
d912d53979 | ||
|
|
ded0d401c7 | ||
|
|
0fdc6e435b | ||
|
|
91d1a7994e | ||
|
|
a4f508feed | ||
|
|
d2f8e57118 | ||
|
|
7dd1176545 | ||
|
|
97fea6e809 | ||
|
|
1809416dce | ||
|
|
d75aea1a09 | ||
|
|
d209ab94ba | ||
|
|
afa8fd307a | ||
|
|
84472cc276 | ||
|
|
d0ddc9ae8c | ||
|
|
1815de059e | ||
|
|
12ad2bf14b | ||
|
|
117087d56b | ||
|
|
2ee3103c8c | ||
|
|
b8a2d7a555 | ||
|
|
78054b2c1d | ||
|
|
30dae7d7ed | ||
|
|
548e64c963 | ||
|
|
e8a63ec40d | ||
|
|
5630bedc51 | ||
|
|
9ef18b6324 | ||
|
|
7b4dfc062a | ||
|
|
8d0504c759 | ||
|
|
5ce2877fc0 | ||
|
|
c02222610b | ||
|
|
360a6efafc | ||
|
|
74c96ba5e9 | ||
|
|
049480408a | ||
|
|
31b10435d3 | ||
|
|
9331c26b22 | ||
|
|
f72e2fca94 | ||
|
|
223c9a153c | ||
|
|
5a3999fbdb | ||
|
|
ac339bb61f | ||
|
|
33f8dacbfc | ||
|
|
f317d3e989 | ||
|
|
47119c73a4 | ||
|
|
4e7b8f5c1a | ||
|
|
c3512b787e | ||
|
|
470001bbab | ||
|
|
b25070acbd | ||
|
|
e99a1b9450 | ||
|
|
b5f84d4a9b | ||
|
|
16dc3a13aa | ||
|
|
8fda2b0751 | ||
|
|
7ad0aa61af | ||
|
|
e4f9a48c2c | ||
|
|
c08415a255 | ||
|
|
2caa9504c9 | ||
|
|
ee54626de9 | ||
|
|
80bd17a28e | ||
|
|
ba17f9908d | ||
|
|
27f3726124 | ||
|
|
97c460e2f1 | ||
|
|
945c75ea07 | ||
|
|
7f7bb1c939 | ||
|
|
7720b7036d | ||
|
|
1b85406b13 | ||
|
|
f85f45fbd3 | ||
|
|
4a4397ace1 | ||
|
|
240568870a | ||
|
|
29313cce54 | ||
|
|
386998598e | ||
|
|
d934978fb2 | ||
|
|
ce461521e4 | ||
|
|
50fad4ceff | ||
|
|
ce46677dfb | ||
|
|
b3c698be25 | ||
|
|
c1d7460254 | ||
|
|
ab01c6dc48 | ||
|
|
c91bb2828f | ||
|
|
f96443e1a6 | ||
|
|
701a7d0eac | ||
|
|
0055b86b19 | ||
|
|
77acdb2d65 | ||
|
|
125f740a5f | ||
|
|
f5d112a712 | ||
|
|
a5f8b8431d | ||
|
|
499ce21cfc | ||
|
|
643420b644 | ||
|
|
49ca49fa1e | ||
|
|
b4aeb1a3fd | ||
|
|
39297d1a07 | ||
|
|
de67de33df | ||
|
|
bf12bd5b05 | ||
|
|
990713c7a7 | ||
|
|
a4e6683c5b | ||
|
|
06bd9947ce | ||
|
|
305eacbe95 | ||
|
|
876a5a65ec | ||
|
|
3e7197702b | ||
|
|
c815d06ab0 | ||
|
|
7a5a94bb64 | ||
|
|
1ac7a40432 | ||
|
|
d209049f71 | ||
|
|
5677e8ec5f | ||
|
|
cd8a5392d5 | ||
|
|
8b6c72b704 | ||
|
|
4d03a2db53 | ||
|
|
b6652c7c7f | ||
|
|
ac89601fc5 | ||
|
|
7e693fcc97 | ||
|
|
84246bea89 | ||
|
|
2aafe172d6 | ||
|
|
359770ecb4 | ||
|
|
366cf94e15 | ||
|
|
c0c4310f08 | ||
|
|
ef0fc85eb9 | ||
|
|
88a2b66050 | ||
|
|
be7c3c5811 | ||
|
|
9c01535c78 | ||
|
|
3649e45087 | ||
|
|
35521fe188 | ||
|
|
5937904b4a | ||
|
|
98655284b2 | ||
|
|
6743f84146 | ||
|
|
3010fe25db | ||
|
|
a12665bbd1 | ||
|
|
e1abcb5d69 | ||
|
|
5c75487446 | ||
|
|
f3bc2909e1 | ||
|
|
acf374df4e | ||
|
|
e6e276aed3 | ||
|
|
541518db81 | ||
|
|
9781f138ed | ||
|
|
f655f2c1d5 | ||
|
|
8d317dbb85 | ||
|
|
650b65078a | ||
|
|
2c7bd78a64 | ||
|
|
9f37c54a3c | ||
|
|
866ca0923d | ||
|
|
75bd6f1ce1 | ||
|
|
5c12ba253e | ||
|
|
485e655fd8 | ||
|
|
dc7cebaa65 | ||
|
|
126aa9df10 | ||
|
|
4ae8fc7c45 | ||
|
|
2fac28b7e9 | ||
|
|
8e3ec4cb84 | ||
|
|
b2645aed9d | ||
|
|
05f2a3602a | ||
|
|
3b922ced78 | ||
|
|
43a600c5a5 | ||
|
|
01d7fd5bef | ||
|
|
98d1e40bf3 | ||
|
|
e96b813fcb | ||
|
|
f15c97d11a | ||
|
|
6a5bb84d71 | ||
|
|
0e9478d033 | ||
|
|
47b713ea69 | ||
|
|
cee5b26c13 | ||
|
|
7c8470f44a | ||
|
|
3477a8844c | ||
|
|
3f6cc5da52 | ||
|
|
f2c5187ef1 | ||
|
|
da0a9ad316 | ||
|
|
fec21ab5c5 | ||
|
|
ccc9210232 | ||
|
|
325abd8d6f | ||
|
|
1d7f70b31c | ||
|
|
eda8dfbc91 | ||
|
|
8206e53b5c | ||
|
|
79701feef1 | ||
|
|
f42b085e12 | ||
|
|
60aaa16327 | ||
|
|
ad20f954c5 | ||
|
|
e708117259 | ||
|
|
5def12351f | ||
|
|
44cfebf898 | ||
|
|
63ccff67a2 | ||
|
|
50005b108f | ||
|
|
727106f4e9 | ||
|
|
c251de4f77 | ||
|
|
8bee8c10ff | ||
|
|
214a98efb1 | ||
|
|
e06e9b0e22 | ||
|
|
22b62699ac | ||
|
|
5f0fff5fd6 | ||
|
|
2a2394b34d | ||
|
|
2e0eb89ad3 | ||
|
|
6bd14c53b5 | ||
|
|
149e4d43e5 | ||
|
|
c72a66ba06 | ||
|
|
736fb92ed7 | ||
|
|
ef67e3ed2c | ||
|
|
1d984f0d0b | ||
|
|
cff92f3dac | ||
|
|
f953ab5a8f | ||
|
|
5f5d4f964a | ||
|
|
7fa7a42673 | ||
|
|
df400a90aa | ||
|
|
14e2cdc23d | ||
|
|
bbd13215a4 | ||
|
|
e73fab0a73 | ||
|
|
035a2eb6d0 | ||
|
|
57d2828496 | ||
|
|
8dbfb4d297 | ||
|
|
29d91d4b19 | ||
|
|
661cf032fe | ||
|
|
cd5e658fb9 | ||
|
|
8883002e3f | ||
|
|
2cc9834e4c | ||
|
|
83c652a810 | ||
|
|
3a6dacf378 | ||
|
|
72bcbd98e8 | ||
|
|
6a92eec983 | ||
|
|
3adc4a0be4 | ||
|
|
bf0b90d362 | ||
|
|
00b78fd629 | ||
|
|
5ddfc582f7 | ||
|
|
db2771e8de | ||
|
|
29a8fb2531 | ||
|
|
061eb0f0f8 | ||
|
|
17606c66b8 | ||
|
|
34349bd663 | ||
|
|
e9982a2db6 | ||
|
|
dd034a9d42 | ||
|
|
cacd377d55 | ||
|
|
dad67360b9 | ||
|
|
6e596f34a5 | ||
|
|
4e1c36955a | ||
|
|
e5d37f6e74 | ||
|
|
7addfb5aa3 | ||
|
|
b4ebaa8863 | ||
|
|
fe7734b9e0 | ||
|
|
10f197c817 | ||
|
|
0a5b9b50f1 | ||
|
|
d9821fa89c | ||
|
|
e3d9ac9d31 | ||
|
|
d5ed19ee34 | ||
|
|
ba3dd70f6c | ||
|
|
ba3dfa9fcc | ||
|
|
f91894aa1b | ||
|
|
90d3206071 | ||
|
|
d372438b6e | ||
|
|
d2b96d1340 | ||
|
|
a32eef9ed1 | ||
|
|
c7be1ebbbe | ||
|
|
391059c967 | ||
|
|
bea9ca88a6 | ||
|
|
1b659335b9 | ||
|
|
61e3ad9d24 | ||
|
|
406f569989 | ||
|
|
3294f67dbf | ||
|
|
2dc7e8d206 | ||
|
|
3b894753b1 | ||
|
|
952fbe68e2 | ||
|
|
99382cf823 | ||
|
|
ceab874abc | ||
|
|
2354e94347 | ||
|
|
2ba011d3fd | ||
|
|
179aa0ef35 | ||
|
|
bb01f243fe | ||
|
|
3635bcf3a9 | ||
|
|
e60b03916a | ||
|
|
9a24ac6bdf | ||
|
|
f71e94c9d8 | ||
|
|
8906e29102 | ||
|
|
9bd113054c | ||
|
|
9324b8af8c | ||
|
|
585834ffbe | ||
|
|
96fa1990b0 | ||
|
|
0ee431b490 | ||
|
|
8990574c1d | ||
|
|
a0a4493ef2 | ||
|
|
77ca1ec3dd | ||
|
|
2e7da4f3cc | ||
|
|
a97589e4a6 | ||
|
|
53c32e5669 | ||
|
|
5429431567 | ||
|
|
6272c8a4ad | ||
|
|
89f031f4f9 | ||
|
|
ba29adf6ef | ||
|
|
61132a3e84 | ||
|
|
db8a5ee2df | ||
|
|
e47ae480f5 | ||
|
|
f900cda9c0 | ||
|
|
38eaae8bb6 | ||
|
|
d2e62874bb | ||
|
|
a96d5926c7 | ||
|
|
72e2c89e3b | ||
|
|
bb256dccdc | ||
|
|
a043dc9a42 | ||
|
|
98dcaae853 | ||
|
|
eee67ac9d9 | ||
|
|
af580d9d90 | ||
|
|
039c0f71f5 | ||
|
|
150b3c3bdb | ||
|
|
1dcf6a79ce | ||
|
|
18661f0ef0 | ||
|
|
ef76ca5de3 | ||
|
|
dac5f98052 | ||
|
|
c2c202e716 | ||
|
|
c1b1713ab1 | ||
|
|
e8adf03826 | ||
|
|
1209d8e4b7 | ||
|
|
e09ee919a5 | ||
|
|
c9743ee85d | ||
|
|
0c613eacbc | ||
|
|
6c692399c2 | ||
|
|
4fc1f31651 | ||
|
|
d4ca812578 | ||
|
|
8502d6f2d7 | ||
|
|
ee851b4aa6 | ||
|
|
f2a5dde761 | ||
|
|
e4f2e647d7 | ||
|
|
c50d5066a9 | ||
|
|
2f707366f3 | ||
|
|
6f544b4ca2 | ||
|
|
0ee1163a0b | ||
|
|
938289a6f0 | ||
|
|
36fceec6b2 | ||
|
|
fc3518f5ba | ||
|
|
5c23c10c04 | ||
|
|
1389bac261 | ||
|
|
47caca82a1 | ||
|
|
baeaef3ab1 | ||
|
|
4f9fefe9e5 | ||
|
|
fdaa20977d | ||
|
|
11f1a14063 | ||
|
|
0d7f7630b0 | ||
|
|
6704a18897 | ||
|
|
24c64967fd | ||
|
|
85b4af5b1d | ||
|
|
2e66cdb122 | ||
|
|
b57221f6b5 | ||
|
|
1c53ef597d | ||
|
|
0ba8380db4 | ||
|
|
f090d697c4 | ||
|
|
3cfdb9e6ec | ||
|
|
034be69ac1 | ||
|
|
b852058501 | ||
|
|
b3830e0264 | ||
|
|
c19f835334 | ||
|
|
9f907d0f31 | ||
|
|
152ebfef48 | ||
|
|
3d5f009380 | ||
|
|
3735b78328 | ||
|
|
709b0a3271 | ||
|
|
8c5e4ab691 | ||
|
|
a581cb2086 | ||
|
|
f433413c49 | ||
|
|
164a3a1f0f | ||
|
|
9aa56e6b56 | ||
|
|
76d2e58f19 | ||
|
|
01b0992c39 | ||
|
|
735ac36579 | ||
|
|
6beae21ec4 | ||
|
|
8b8b73983b | ||
|
|
37e897d0d2 | ||
|
|
5c83aac96e | ||
|
|
39dd038831 | ||
|
|
ff69552a61 | ||
|
|
b54307a0df | ||
|
|
65f05c86ed | ||
|
|
5eb1c86acf | ||
|
|
b8f8818eef | ||
|
|
89c6482a96 | ||
|
|
d44b4bc682 | ||
|
|
802134c846 | ||
|
|
1969ca5535 | ||
|
|
5084670820 | ||
|
|
e78eaf45f7 | ||
|
|
a877ba8a73 | ||
|
|
a4e899807c | ||
|
|
d99b0e5525 | ||
|
|
46ac9a03b9 | ||
|
|
57bf4e490d | ||
|
|
649be77fa3 | ||
|
|
5c1bb06294 | ||
|
|
284b603e6e | ||
|
|
e92e08e5cc | ||
|
|
3dc682087a | ||
|
|
ebe3232268 | ||
|
|
0eae531706 | ||
|
|
e9e93ef26b | ||
|
|
fe4791d6f5 | ||
|
|
6332e75e61 | ||
|
|
856fbc61b1 | ||
|
|
47948fe847 | ||
|
|
0b1a081e34 | ||
|
|
0172aa3081 | ||
|
|
4096691eb2 | ||
|
|
fc63b801f9 | ||
|
|
af1f3cc007 | ||
|
|
6b18ad45ea | ||
|
|
a8cd802aad | ||
|
|
9a648219cf | ||
|
|
6749d09025 | ||
|
|
0d2d7c913d | ||
|
|
090964a786 | ||
|
|
fcbdbd6002 | ||
|
|
c6bf960eec | ||
|
|
ab4b75986d | ||
|
|
7144bbe694 | ||
|
|
03814b9660 | ||
|
|
bccf8c6b38 | ||
|
|
1308e8201e | ||
|
|
aa3fb9cac7 | ||
|
|
22342b310c | ||
|
|
2d9616eca5 | ||
|
|
7db3008e5c | ||
|
|
31d0db939a | ||
|
|
bc49b452f9 | ||
|
|
99359f987c | ||
|
|
cb9ef94501 | ||
|
|
94c482ed35 | ||
|
|
ef48fa2a37 | ||
|
|
576c79657b | ||
|
|
3db2130b56 | ||
|
|
0b9b395879 | ||
|
|
35ad47c653 | ||
|
|
984153b90a | ||
|
|
9cd285ddb0 | ||
|
|
ca5a374042 | ||
|
|
ee8baae160 | ||
|
|
f10078e482 | ||
|
|
0282fe7e17 | ||
|
|
4142c7691a | ||
|
|
223bb145e6 | ||
|
|
c9ce8d3a74 | ||
|
|
bee7bdaa70 | ||
|
|
695741d9d3 | ||
|
|
01324d2ebb | ||
|
|
3d6c843958 | ||
|
|
97495377b2 | ||
|
|
1174d71a06 | ||
|
|
108e97b550 | ||
|
|
a4e080da91 | ||
|
|
f11592be3e | ||
|
|
61df1204f8 | ||
|
|
df474a33d0 | ||
|
|
c10e0c7e3c | ||
|
|
a07c53a397 | ||
|
|
921340ae60 | ||
|
|
fabd7ac91f | ||
|
|
fec1916a58 | ||
|
|
e90ad5acbd | ||
|
|
ddc386d16c | ||
|
|
007f5d21b2 | ||
|
|
67152f6b7b | ||
|
|
ee36c578e2 | ||
|
|
0b202bb3e7 | ||
|
|
fc80186043 | ||
|
|
d6f935d779 | ||
|
|
109936f846 | ||
|
|
09e86a16f6 | ||
|
|
d83e85b8ca | ||
|
|
c2179b3959 | ||
|
|
36c4ff6050 | ||
|
|
b31aa9f71d | ||
|
|
18fe51ad25 | ||
|
|
12c0de503d | ||
|
|
804007c997 | ||
|
|
6d3c555ac4 | ||
|
|
fa79b7d30c | ||
|
|
5f9cb86ff0 | ||
|
|
a62540800b | ||
|
|
c250e703f7 | ||
|
|
197696075c | ||
|
|
78e0312fee | ||
|
|
1b406202b5 | ||
|
|
2e70c3baed | ||
|
|
30106c8f93 | ||
|
|
b14f00a4b2 | ||
|
|
753aa4217d | ||
|
|
b6f80668d1 | ||
|
|
a33666b778 | ||
|
|
ec82d95a03 | ||
|
|
2c5deafe56 | ||
|
|
9cc996b775 | ||
|
|
2967d2fb71 | ||
|
|
d9d4a7807f | ||
|
|
831018f161 | ||
|
|
84a171dcac | ||
|
|
8de64174bb | ||
|
|
69a99c7860 | ||
|
|
c85a8116ef | ||
|
|
b16da44e42 | ||
|
|
7ae2f74643 | ||
|
|
482d2787fb | ||
|
|
12d4e2433e | ||
|
|
70b55e4626 | ||
|
|
e836090a02 | ||
|
|
4b499a076a | ||
|
|
b0ca182fc5 | ||
|
|
45a6320b5e | ||
|
|
e4fe534382 | ||
|
|
336b6871f3 | ||
|
|
e25d5cc220 | ||
|
|
101266d1a7 | ||
|
|
efdfb1d835 | ||
|
|
4e2cc0e35a | ||
|
|
08865b2909 | ||
|
|
0af9a3092b | ||
|
|
dc0dbbf720 | ||
|
|
91909fbb1e | ||
|
|
ce8144b79e | ||
|
|
0c09252dd6 | ||
|
|
98529b3268 | ||
|
|
0dcecdd7b2 | ||
|
|
44522027e7 | ||
|
|
3f4a67f552 | ||
|
|
4afd5df55f | ||
|
|
991fc0a2c1 | ||
|
|
4908cb7e19 | ||
|
|
c16334d551 | ||
|
|
e0ba199e20 | ||
|
|
d6c6a4d111 | ||
|
|
62a853e73c | ||
|
|
ff2eae2efb | ||
|
|
ff9d205d5b | ||
|
|
6437665a53 | ||
|
|
aec9536522 | ||
|
|
f843b553c7 | ||
|
|
d9c8e006b5 | ||
|
|
361c7df812 | ||
|
|
a1fdca09f6 | ||
|
|
7506da41f0 | ||
|
|
17777c540b | ||
|
|
91081fc5c7 | ||
|
|
3eee716b50 | ||
|
|
0448a84286 | ||
|
|
3baaa42c82 | ||
|
|
39594ad800 | ||
|
|
414d40c00c | ||
|
|
cb32aa5aac | ||
|
|
069ecc900a | ||
|
|
ec66a7e4f5 | ||
|
|
2a9a92cc34 | ||
|
|
9b879d6ced | ||
|
|
5805edc3dc | ||
|
|
3f9edc88c4 | ||
|
|
5a9e61164b | ||
|
|
8bd6e4fb2d | ||
|
|
e8a4b8dd4e | ||
|
|
61f2ca7a88 | ||
|
|
bfdd9cde15 | ||
|
|
b14bfa95c2 | ||
|
|
42efbbc7da | ||
|
|
8f908c320a | ||
|
|
5ed813fe06 | ||
|
|
7eefb499f3 | ||
|
|
b4a9339459 | ||
|
|
afc54eae00 | ||
|
|
7244f22722 | ||
|
|
973c734226 | ||
|
|
4a6765c49a | ||
|
|
db91bccda3 | ||
|
|
11fc932905 | ||
|
|
f7ff2df9a7 | ||
|
|
3a6d075cbb | ||
|
|
f0e68edad1 | ||
|
|
1ed467d236 | ||
|
|
8785fc358a | ||
|
|
400b9fa93c | ||
|
|
2e9babe73b | ||
|
|
7bfa75b32c | ||
|
|
884eb4b412 | ||
|
|
ac17ffda54 | ||
|
|
468aeeebc7 | ||
|
|
11b6f78924 | ||
|
|
a630401d10 | ||
|
|
4abfbc3e9c | ||
|
|
6a3def3c9a | ||
|
|
e97885eb22 | ||
|
|
51e7f821e9 | ||
|
|
6e5f86b2b5 | ||
|
|
e0a287d797 | ||
|
|
39946e3cd9 | ||
|
|
eb9d379efc | ||
|
|
93a6277ca4 | ||
|
|
043257cabf | ||
|
|
6d8c28f228 | ||
|
|
c61a6da884 | ||
|
|
9640a18b03 | ||
|
|
4049a3f884 | ||
|
|
e8ca27b590 | ||
|
|
7d6b45ba5d | ||
|
|
04e8a18c9a | ||
|
|
17f5c0ecbd | ||
|
|
d4cef49f88 | ||
|
|
49ee66d49c | ||
|
|
b631c4ed72 | ||
|
|
91a3a621d0 | ||
|
|
2e4ed0347b | ||
|
|
7f3247bfcc | ||
|
|
fddbba217d | ||
|
|
44af542f61 | ||
|
|
2845270016 | ||
|
|
d149bad5fc | ||
|
|
f054f9c8d9 | ||
|
|
a1e8bfe80a | ||
|
|
06c35839ef | ||
|
|
fe7aabb44a | ||
|
|
c7c47d5ca3 | ||
|
|
dff2da0522 | ||
|
|
1ab51f3da1 | ||
|
|
161af93347 | ||
|
|
f72fb177ba | ||
|
|
30102f5f42 | ||
|
|
d28b7062d2 | ||
|
|
22e3c1b0a5 | ||
|
|
368f629948 | ||
|
|
5f79d00f7a | ||
|
|
7aada92be9 | ||
|
|
721160fbd9 | ||
|
|
4463f8870a | ||
|
|
fde359b4af | ||
|
|
ba0b21b999 | ||
|
|
adf1e9507a | ||
|
|
3ea830fa00 | ||
|
|
ec0666ca2a | ||
|
|
484d6f2fa4 | ||
|
|
a9e407a39f | ||
|
|
c6390575ad | ||
|
|
7773a0c648 | ||
|
|
38257a7f54 | ||
|
|
49e898b6f3 | ||
|
|
562901c84f | ||
|
|
eebef4d14f | ||
|
|
68df26d4a5 | ||
|
|
e811ac395a | ||
|
|
68037b25a5 | ||
|
|
99e9b1ae85 | ||
|
|
efebf8c5e8 | ||
|
|
f408abcf4e | ||
|
|
3aa0cd4c06 | ||
|
|
34a151e4cb | ||
|
|
e4a84fd997 | ||
|
|
37485820b5 | ||
|
|
653853588a | ||
|
|
1bb885fd11 | ||
|
|
7eb6d6ee68 | ||
|
|
15d460ee7c | ||
|
|
a74a62079e | ||
|
|
6d54a7c102 | ||
|
|
adc8b2a670 | ||
|
|
b16a4995b7 | ||
|
|
e85a8c8310 | ||
|
|
f3b1300628 | ||
|
|
2c407540a0 | ||
|
|
e95fc090df | ||
|
|
bcda9b82f4 | ||
|
|
d5174262d8 | ||
|
|
61818572cc | ||
|
|
6496448569 | ||
|
|
70fd5c077f | ||
|
|
e61fc8e392 | ||
|
|
5cbab70035 | ||
|
|
376695e410 | ||
|
|
c3468987cb | ||
|
|
b6d8e822b4 | ||
|
|
ddcc36aa26 | ||
|
|
7e88d15e1d | ||
|
|
4a7f37b920 | ||
|
|
bd163fffff | ||
|
|
e96e6b7751 | ||
|
|
0d618964a7 | ||
|
|
d62fae9e88 | ||
|
|
247479e411 | ||
|
|
1108c773dc | ||
|
|
bc42df12f5 | ||
|
|
2caa653f02 | ||
|
|
16a9094770 | ||
|
|
c6cb3f0e8e | ||
|
|
1da667b09e | ||
|
|
f2e35da1fe | ||
|
|
33c98a014f | ||
|
|
261198ab47 | ||
|
|
77f2cdc34c | ||
|
|
40b8e1a740 | ||
|
|
3279aa86ce | ||
|
|
e5d8c34514 | ||
|
|
7e19cbaf3a | ||
|
|
a069d11ef9 | ||
|
|
a49cf5434e | ||
|
|
1f5b42845b | ||
|
|
b1bf094bfd | ||
|
|
066fa8552d | ||
|
|
0f14abd6c8 | ||
|
|
a444f28134 | ||
|
|
87501a24fe | ||
|
|
033c24881c | ||
|
|
3dbbe02f69 | ||
|
|
1f4266f8b1 | ||
|
|
2cf0f9c928 | ||
|
|
aaa05e761e | ||
|
|
4f0870b961 | ||
|
|
fc79fa7345 | ||
|
|
f71e5531e0 | ||
|
|
00ca006d26 | ||
|
|
29ee55d751 | ||
|
|
aa58c7fd3d | ||
|
|
f8d0acb38d | ||
|
|
0218ea9ddd | ||
|
|
9299c95f10 | ||
|
|
a9a65d53bf | ||
|
|
7cddb47edc | ||
|
|
8cd54f9802 | ||
|
|
a014d518bf | ||
|
|
2f5a49b011 | ||
|
|
918a3cc061 | ||
|
|
1f026baf45 | ||
|
|
a8c81f7e2b | ||
|
|
255853dd76 | ||
|
|
587a980081 | ||
|
|
bbc8b7693a | ||
|
|
9edc76e5eb | ||
|
|
7f4a1a66fa | ||
|
|
0c16c2b09a | ||
|
|
67d1718450 | ||
|
|
e86de44259 | ||
|
|
4966600d52 | ||
|
|
6686dd1815 | ||
|
|
8dc7595e46 | ||
|
|
1d05ff34c6 | ||
|
|
44e0b7849e | ||
|
|
0a615913eb | ||
|
|
8f8c8c267f | ||
|
|
2c31e3fe03 | ||
|
|
39e94fdfd3 | ||
|
|
cf869770fb | ||
|
|
b0933eb282 | ||
|
|
ebc96712a2 | ||
|
|
c1048686d1 | ||
|
|
0a05a2095e | ||
|
|
52f4628fe0 | ||
|
|
e068fe5d3a | ||
|
|
da2c13dcf1 | ||
|
|
f99a8c1da2 | ||
|
|
ef476bf4e2 | ||
|
|
48d7f2b21b | ||
|
|
17444b8c8a | ||
|
|
60b4870549 | ||
|
|
865e4cda59 | ||
|
|
e6db9fef60 | ||
|
|
0af35630d1 | ||
|
|
a4ca31f406 | ||
|
|
d0aa60fe1a | ||
|
|
d40f58d79a | ||
|
|
3e1cc8af85 | ||
|
|
8676893d59 | ||
|
|
04ff801531 | ||
|
|
6437bf780b | ||
|
|
4f4e63bb0b | ||
|
|
f6cc3b6e53 | ||
|
|
982e5a06ab | ||
|
|
8121f93d2e | ||
|
|
641a8318a8 | ||
|
|
a0fbf51be0 | ||
|
|
17dd06221a | ||
|
|
cdc5474279 | ||
|
|
cee3228e5f | ||
|
|
864a250eea | ||
|
|
3f4df4233b | ||
|
|
1f9dbd9dea | ||
|
|
7d69404532 | ||
|
|
49dc837df2 | ||
|
|
f0314ab51c | ||
|
|
3be5cdd365 | ||
|
|
96af98f063 | ||
|
|
f6bfd36a6c | ||
|
|
1350c95db6 | ||
|
|
bc7864466c | ||
|
|
fad96603a7 | ||
|
|
894b24698e | ||
|
|
5e146f20bc | ||
|
|
539bfd6db9 | ||
|
|
c36fd3bfc8 | ||
|
|
feccea21a5 | ||
|
|
37b80b612b | ||
|
|
5b4b17ae49 | ||
|
|
43c300cba3 | ||
|
|
cbe6da09ee | ||
|
|
27fd9ee62d | ||
|
|
6bd6ea540c | ||
|
|
7e5b60aa10 | ||
|
|
7d71b08def | ||
|
|
a221452cb6 | ||
|
|
863441c418 | ||
|
|
ab22277e7b | ||
|
|
32eaf9df04 | ||
|
|
af6cd31e71 | ||
|
|
87b8c6bd3a | ||
|
|
5f3725b72f | ||
|
|
d1080129d2 | ||
|
|
ea6b895df9 | ||
|
|
f27419165d | ||
|
|
6e9a50f2ec | ||
|
|
07aa436380 | ||
|
|
c4c11ad813 | ||
|
|
fd8054bc7b | ||
|
|
3a793e25b3 | ||
|
|
9970b2235c | ||
|
|
4134200b4e | ||
|
|
ccae89fb60 | ||
|
|
1a936f33ef | ||
|
|
68fb29e385 | ||
|
|
f0cc198f72 | ||
|
|
17491082d4 | ||
|
|
c80e82d104 | ||
|
|
50efddd5f0 | ||
|
|
40eafafe3d | ||
|
|
348cd855b9 | ||
|
|
a175129f93 | ||
|
|
9c7442544b | ||
|
|
cc8e47cfc8 | ||
|
|
0279ed84a9 | ||
|
|
7b1143b9d4 | ||
|
|
27fb1426d9 | ||
|
|
eae3ef5c32 | ||
|
|
b3c0a2fa7b | ||
|
|
15dbb6fe51 | ||
|
|
9c70eab075 | ||
|
|
15d0772a20 | ||
|
|
57888f8d9e | ||
|
|
c77066162a | ||
|
|
b9d41d179d | ||
|
|
73019cfd83 | ||
|
|
22aa5d47a5 | ||
|
|
8efe075679 | ||
|
|
714370a1a2 | ||
|
|
f316554ca3 | ||
|
|
e7be94a3d2 | ||
|
|
c4217a8aa6 | ||
|
|
3d277349be | ||
|
|
c81e8e5025 | ||
|
|
e9e38ee029 | ||
|
|
ed3815a406 | ||
|
|
fe03feaf4a | ||
|
|
e8365fdec6 | ||
|
|
5c772aeecf | ||
|
|
2bcba58131 | ||
|
|
b6a5f98d41 | ||
|
|
d0173e1b73 | ||
|
|
f8d95fcb25 | ||
|
|
35ce2a2b93 | ||
|
|
3b47a56076 | ||
|
|
46f4ce7091 | ||
|
|
4062ecdfb1 | ||
|
|
8e3ff6fd81 | ||
|
|
2f9dd6a27b | ||
|
|
a816b842e5 | ||
|
|
56897368b3 | ||
|
|
f67f090b2a | ||
|
|
e3745f279e | ||
|
|
8173677475 | ||
|
|
74bd4eed7c | ||
|
|
ae461e746f | ||
|
|
8f83167c0f | ||
|
|
6d3187db57 | ||
|
|
5276cd0fa0 | ||
|
|
80c81502fb | ||
|
|
9bfdf3ea41 | ||
|
|
dc156c7f2b | ||
|
|
d946a1b93a | ||
|
|
cebd138190 | ||
|
|
8b327a3f27 | ||
|
|
972910c763 | ||
|
|
9842de7ab7 | ||
|
|
c04f51d424 | ||
|
|
76c02012eb | ||
|
|
c0a4bc7d89 | ||
|
|
9279ded7f3 | ||
|
|
3e1641aaf0 | ||
|
|
f2d2e6be8f | ||
|
|
405755f06a | ||
|
|
b8ddf0f52d | ||
|
|
e642551dfe | ||
|
|
2fe5f34c5c | ||
|
|
8fb9129300 | ||
|
|
eceb69c58e | ||
|
|
7582509792 | ||
|
|
1988afa84d | ||
|
|
247e3a2efe | ||
|
|
694ed8e263 | ||
|
|
78bed5efe4 | ||
|
|
8ae45e9e39 | ||
|
|
f91251be14 | ||
|
|
e8a3d5fa16 | ||
|
|
cbb1a03ccd | ||
|
|
61ef23b885 | ||
|
|
4fbb1dafe1 | ||
|
|
8029b6e3f5 | ||
|
|
02bd10b005 | ||
|
|
9a06d19984 | ||
|
|
e6bf925a94 | ||
|
|
64d46cdef0 | ||
|
|
d1034cb4ea | ||
|
|
c8236373b1 | ||
|
|
e76dc6cb82 | ||
|
|
69f04dec87 | ||
|
|
092e3e3cdc | ||
|
|
fb9e7c3351 | ||
|
|
db6acf8662 | ||
|
|
594017ba5f | ||
|
|
4fdecb697b | ||
|
|
359a327ec4 | ||
|
|
3b641a2423 | ||
|
|
3354c0b374 | ||
|
|
25e2ffeea7 | ||
|
|
f7e1752fc2 | ||
|
|
977748b16c | ||
|
|
c259a7330b | ||
|
|
0706ccce8a | ||
|
|
aca10e8268 | ||
|
|
e913a27f0c | ||
|
|
8982bb034e | ||
|
|
0773a82342 | ||
|
|
b771e95997 | ||
|
|
5b78ec04f6 | ||
|
|
5dea9394bf | ||
|
|
5d1b8d26e7 | ||
|
|
78a655bb05 | ||
|
|
27d93ecaf9 | ||
|
|
972c6b7f0a | ||
|
|
7423133229 | ||
|
|
5e309c27ca | ||
|
|
8feb7902b9 | ||
|
|
bba44fbe11 | ||
|
|
bc6d372064 | ||
|
|
e4c591e1e6 | ||
|
|
05064f7f3d | ||
|
|
3cfa9f8da1 | ||
|
|
586f62fd0a | ||
|
|
2c5293bff0 | ||
|
|
a154bacffb | ||
|
|
9aaac3cb20 | ||
|
|
577d1bd72f | ||
|
|
d62916edf0 | ||
|
|
45efaf095d | ||
|
|
24906283fe | ||
|
|
9465ed4939 | ||
|
|
0dbdd47130 | ||
|
|
341da73398 | ||
|
|
ba7146f90a | ||
|
|
9005c67b8b | ||
|
|
0b72364f10 | ||
|
|
be9a953136 | ||
|
|
6e4b1ae9c4 | ||
|
|
ba2bafe56e | ||
|
|
d02c55dbcd | ||
|
|
4fcf45d545 | ||
|
|
ec856cc73f | ||
|
|
4ce7e29a37 | ||
|
|
dc2b6c3d68 | ||
|
|
2e41610693 | ||
|
|
e2472cb5c3 | ||
|
|
c191b777b7 | ||
|
|
0a060d04ca | ||
|
|
a76ba87501 | ||
|
|
01fa1bc552 | ||
|
|
5b321c6304 | ||
|
|
3482e4a327 | ||
|
|
394ac93dc4 | ||
|
|
93c0c5bdfc | ||
|
|
b6fd489b8e | ||
|
|
35266d2a31 | ||
|
|
d14babea33 | ||
|
|
af27eaa33b | ||
|
|
ec7ec2a229 | ||
|
|
f70d5c7460 | ||
|
|
6ad89a4f6a | ||
|
|
12e09cb0ed | ||
|
|
0ce6e9e83a | ||
|
|
306aaa94ba | ||
|
|
be65962e96 | ||
|
|
a6aea51ff3 | ||
|
|
607bd5d475 | ||
|
|
387f95208d | ||
|
|
fb92f1a8c9 | ||
|
|
4cd52c27d5 | ||
|
|
a5c616ba0f | ||
|
|
65264137f8 | ||
|
|
b390f3c28f | ||
|
|
274f4f2d63 | ||
|
|
eaac611465 | ||
|
|
6a67cc2038 | ||
|
|
865657b3bd | ||
|
|
5caa1131c8 | ||
|
|
cf77fffaeb | ||
|
|
51467296c3 | ||
|
|
334521ce3f | ||
|
|
47082c4e2e | ||
|
|
658c248867 | ||
|
|
6c5c1ef0a0 | ||
|
|
0f9210e491 | ||
|
|
9dd2dffc80 | ||
|
|
81d71690c2 | ||
|
|
38c4c0772b | ||
|
|
69442dcbb8 | ||
|
|
17179c9c4a | ||
|
|
0e5778c013 | ||
|
|
f02894f164 | ||
|
|
2afb7268a1 | ||
|
|
efcc9a1761 | ||
|
|
c800d9253d | ||
|
|
92b41ec300 | ||
|
|
8e25ffb392 | ||
|
|
570e637795 | ||
|
|
b9af72e71d | ||
|
|
f07a5b7a83 | ||
|
|
149c397b7b | ||
|
|
3c3f3896f7 | ||
|
|
68103d0aca | ||
|
|
442a2a7b04 | ||
|
|
d13663eb23 | ||
|
|
e5a87bd043 | ||
|
|
f841c8eb8a | ||
|
|
81f8f6d4d0 | ||
|
|
9bdfb93b50 | ||
|
|
ef62a80eb8 | ||
|
|
55d9e79983 | ||
|
|
fb21f9c1af | ||
|
|
d369ff968d | ||
|
|
b7821121c3 | ||
|
|
56fe1cab95 | ||
|
|
01aa692a56 | ||
|
|
44e75e661e | ||
|
|
fe23f4ca56 | ||
|
|
6bc6d28e9e | ||
|
|
fb68e66ca4 | ||
|
|
6f99ad3103 | ||
|
|
123b47c612 | ||
|
|
20a1211e09 | ||
|
|
a97e8bcc1f | ||
|
|
c19eff67f6 | ||
|
|
5c74b7cd3e | ||
|
|
8358210241 | ||
|
|
de54c2d059 | ||
|
|
40e6bd36c3 | ||
|
|
1805b3dd27 | ||
|
|
163621ddfa | ||
|
|
c1bec9aca5 | ||
|
|
33f276a69e | ||
|
|
a2bafb61c7 | ||
|
|
312c2dd386 | ||
|
|
8820e9a3c9 | ||
|
|
625846fa33 | ||
|
|
afbce0efeb | ||
|
|
9f41415dc7 | ||
|
|
d1e2220e54 | ||
|
|
fa70093866 | ||
|
|
dcb07c9124 | ||
|
|
ffd26c56b4 | ||
|
|
6985a2cf18 | ||
|
|
9ff1f5645f | ||
|
|
099c9b7eac | ||
|
|
13b5136b92 | ||
|
|
c9131fff39 | ||
|
|
50e276cb94 | ||
|
|
9c8f635e63 | ||
|
|
cd57597975 | ||
|
|
cbee3da2c9 | ||
|
|
389fca1de0 | ||
|
|
1e9264735b | ||
|
|
35648a19ce | ||
|
|
23368a5bbe | ||
|
|
66afcc8625 | ||
|
|
df346fc097 | ||
|
|
7ec5f931bb | ||
|
|
f12bab8676 | ||
|
|
f4b273d0be | ||
|
|
1d189195f9 | ||
|
|
dc34faaada | ||
|
|
a7632cfee1 | ||
|
|
1c499dc230 | ||
|
|
01bc6e4cf0 | ||
|
|
bd082a26bf | ||
|
|
d7fb0c83f9 | ||
|
|
ac45554439 | ||
|
|
217e5795bf | ||
|
|
40b974b29c | ||
|
|
bdcf8b5634 | ||
|
|
b1e3193899 | ||
|
|
7ae56f03b3 | ||
|
|
203ae61eda | ||
|
|
c863b80917 | ||
|
|
dd8041b30f | ||
|
|
fe9441d63b | ||
|
|
05476fb102 | ||
|
|
6ccf71b20d | ||
|
|
418329962d | ||
|
|
f2ff3ee5ab | ||
|
|
3b0609fe76 | ||
|
|
8e108425ae | ||
|
|
20322e7ecb | ||
|
|
5092bdf9fe | ||
|
|
be8dbcf25e | ||
|
|
72233d2365 | ||
|
|
79f0f2a07f | ||
|
|
225e11265f | ||
|
|
6da6471204 | ||
|
|
e4a57970f0 | ||
|
|
55b21fdcda | ||
|
|
51a820ac82 | ||
|
|
c900adbc69 | ||
|
|
b1ee7857f7 | ||
|
|
318d72708c | ||
|
|
172cc3a649 | ||
|
|
d4a27d1975 | ||
|
|
c77cb9bb0a | ||
|
|
add016894b | ||
|
|
fd3fc971fa | ||
|
|
fef800ef7a | ||
|
|
1cd366c0e1 | ||
|
|
dbec6e75e8 | ||
|
|
80b4e6f3e9 | ||
|
|
6185869c6d | ||
|
|
038fd173c2 | ||
|
|
14935378bb | ||
|
|
0dc2b0135a | ||
|
|
c58b92ecd7 | ||
|
|
a281165450 | ||
|
|
4f76b4d765 | ||
|
|
d1f03f2f80 | ||
|
|
0d3f2952c1 | ||
|
|
ba409427c6 | ||
|
|
1b3794e9f9 | ||
|
|
555b6fcf86 | ||
|
|
1bd310c859 | ||
|
|
20acf975d9 | ||
|
|
984b9e8123 | ||
|
|
c7310e1fad | ||
|
|
a5f90ec207 | ||
|
|
72d1d3f582 | ||
|
|
7b8e2cd96c | ||
|
|
c1185b62c1 | ||
|
|
7357a63fdf | ||
|
|
adff64e483 | ||
|
|
00e890e0c3 | ||
|
|
142eefaf86 | ||
|
|
7e780bb6a7 | ||
|
|
ef5cd63d9f | ||
|
|
7bd3e0d4de | ||
|
|
18ee21cf93 | ||
|
|
934f1ccd1f | ||
|
|
a90274f24c | ||
|
|
dc1110af4d | ||
|
|
1457275139 | ||
|
|
52e38ff5d6 | ||
|
|
8403697013 | ||
|
|
d9eee4e520 | ||
|
|
e562896d63 | ||
|
|
7fee3ec551 | ||
|
|
1ceb09062f | ||
|
|
570f3dfab3 | ||
|
|
4cabbc32f5 | ||
|
|
3d6f9c1be4 | ||
|
|
76497481f7 | ||
|
|
139356bd18 | ||
|
|
1cdead18de | ||
|
|
b47918fad5 | ||
|
|
55c9d79a01 | ||
|
|
d7a12b24bf | ||
|
|
41fc5902be | ||
|
|
e47c7fb2ac | ||
|
|
84c148acff | ||
|
|
d559cbf473 | ||
|
|
5e9a9059c8 | ||
|
|
27fa3a4812 | ||
|
|
3d0ab35efe | ||
|
|
f39ba840b8 | ||
|
|
6c86e380a8 | ||
|
|
c63343cf2b | ||
|
|
bd550159f0 | ||
|
|
43c89ca1ed | ||
|
|
1740bec94f | ||
|
|
0e0f611d3c | ||
|
|
bd05ab6d3a | ||
|
|
467d2e7605 | ||
|
|
10de4f1c71 | ||
|
|
794d7b70f3 | ||
|
|
e6c78c17b7 | ||
|
|
3cdb3e6580 | ||
|
|
febc3419b0 | ||
|
|
937ef9fcef | ||
|
|
5800902a42 | ||
|
|
cc072cceb2 | ||
|
|
67bbf80769 | ||
|
|
ce5f5bb7d0 | ||
|
|
577bfb4e6f | ||
|
|
fe1d29f007 | ||
|
|
fbf50b093d | ||
|
|
5b0262d53f | ||
|
|
4a2efe21c7 | ||
|
|
7d9f13b8bc | ||
|
|
50e3db81ea | ||
|
|
d8745fd303 | ||
|
|
bc797550d9 | ||
|
|
5ebab7072c | ||
|
|
9ff4e68246 | ||
|
|
00666499fe | ||
|
|
5487bc8c20 | ||
|
|
a3f6b6e86e | ||
|
|
6f66df61f7 | ||
|
|
72b5a9887f | ||
|
|
076cc3c617 | ||
|
|
91a69b939c | ||
|
|
969c085085 | ||
|
|
426fc2f4e8 | ||
|
|
464575ef44 | ||
|
|
a65ef30d78 | ||
|
|
8f5f2b089c | ||
|
|
111432aa64 | ||
|
|
1c51df039f | ||
|
|
9e3a417059 | ||
|
|
e914a1bdc1 | ||
|
|
b36c685cfa | ||
|
|
3c5c9604b3 | ||
|
|
fb37ca747a | ||
|
|
2d21c53b4b | ||
|
|
752c1f140c | ||
|
|
db3cacd33e | ||
|
|
7ebc282eb2 | ||
|
|
0d44ada922 | ||
|
|
52ffb91101 | ||
|
|
745ec3dbd2 | ||
|
|
15958a078e | ||
|
|
61bf235ef4 | ||
|
|
556b0bc7b7 | ||
|
|
f12f61d7ee | ||
|
|
1176411b79 | ||
|
|
74ca98b38f | ||
|
|
961a234d7d | ||
|
|
a73ae13df4 | ||
|
|
d8cbe2e589 | ||
|
|
e1946b208d | ||
|
|
09bc8c35c5 | ||
|
|
f19ff11b36 | ||
|
|
268243feeb | ||
|
|
4be1e62251 | ||
|
|
10b66e2ad0 | ||
|
|
7a41fcf512 | ||
|
|
518df33b0e | ||
|
|
fdec5b65c5 | ||
|
|
ce1f23fe46 | ||
|
|
331970a121 | ||
|
|
e3caf15f32 | ||
|
|
fa9e102271 | ||
|
|
39ce249361 | ||
|
|
b76e6553bc | ||
|
|
546e0c185d | ||
|
|
4503bdfa7e | ||
|
|
c63e93715f | ||
|
|
904a9fbda1 | ||
|
|
01fcefdec5 | ||
|
|
72fee9e245 | ||
|
|
5838e63cde | ||
|
|
0756a6e020 | ||
|
|
792776a1f6 | ||
|
|
52ef7a4b3c | ||
|
|
66c59501fc | ||
|
|
bce17d9cfc | ||
|
|
9745a987ac | ||
|
|
1a8072e9b2 | ||
|
|
26e6c05313 | ||
|
|
ec51b2d61f | ||
|
|
7374d1648c | ||
|
|
de714f5023 | ||
|
|
3e25bcef02 | ||
|
|
1b92b4885b | ||
|
|
10d3ecfda4 | ||
|
|
03f71fd94e | ||
|
|
59f4802904 | ||
|
|
3131dcbb7c | ||
|
|
018c8e795d | ||
|
|
0209bcf5d3 | ||
|
|
44d4265084 | ||
|
|
f6e6e2c73f | ||
|
|
8aaac45020 | ||
|
|
cc2d2b5f78 | ||
|
|
265421eb96 | ||
|
|
a20d73d2be | ||
|
|
4353daf913 | ||
|
|
3a4daf6e77 | ||
|
|
ab5d1c5f91 | ||
|
|
08e8b2f1cc | ||
|
|
3f7b4351cd | ||
|
|
ceaf466b1c | ||
|
|
fb4343fca6 | ||
|
|
cc269e810c | ||
|
|
b90dc752d2 | ||
|
|
7198154106 | ||
|
|
d237dfec7c | ||
|
|
4f29839de5 | ||
|
|
b46fd599fa | ||
|
|
0e625da109 | ||
|
|
93becd36e6 | ||
|
|
a3e890b711 | ||
|
|
a3df1720b4 | ||
|
|
331c7849f0 | ||
|
|
945c6a82b0 | ||
|
|
cc0dcb071e | ||
|
|
3d9c79e752 | ||
|
|
b39b85db13 | ||
|
|
9f89fd0f7a | ||
|
|
0a1ba79d6f | ||
|
|
e4e9fca243 | ||
|
|
5628138d16 | ||
|
|
8a9d8d0097 | ||
|
|
2f3001458f | ||
|
|
845af9200d | ||
|
|
328d36376c | ||
|
|
6e2a2f0011 | ||
|
|
9793733cd5 | ||
|
|
19dc076528 | ||
|
|
6a0373ee73 | ||
|
|
570b421ba0 | ||
|
|
ee54bd52b3 | ||
|
|
e885fe30f5 | ||
|
|
cd0757e272 | ||
|
|
f5fece4959 | ||
|
|
01987a75d0 | ||
|
|
9d4538935d | ||
|
|
6ee9bad17d | ||
|
|
a5047c29f7 | ||
|
|
2f1545de38 | ||
|
|
de850db548 | ||
|
|
39a1da4c28 | ||
|
|
d1432b7d19 | ||
|
|
a7a3666d85 | ||
|
|
ba1c2d85d3 | ||
|
|
738cfe7835 | ||
|
|
27e66f1092 | ||
|
|
1bde2eca35 | ||
|
|
16237b0df8 | ||
|
|
fae0a7c069 | ||
|
|
b33caa56b8 | ||
|
|
c36cebe357 | ||
|
|
90520a83ff | ||
|
|
6b92964794 | ||
|
|
688785fc6e | ||
|
|
5e955168d2 | ||
|
|
841b3042cf | ||
|
|
4a785772be | ||
|
|
8a9184675a | ||
|
|
b96ed8bd00 | ||
|
|
398078a28f | ||
|
|
6c42390fc2 | ||
|
|
1de1409699 | ||
|
|
734a414186 | ||
|
|
6d3ad965ee | ||
|
|
8bfb456161 | ||
|
|
1340ab123b | ||
|
|
e7fc69bb1d | ||
|
|
1fce405204 | ||
|
|
e6a4ffd418 | ||
|
|
1f1df4f437 | ||
|
|
51a5c5bcbf | ||
|
|
51d6d22a27 | ||
|
|
21d9a4e5b7 | ||
|
|
5293fdd3a9 | ||
|
|
2de554f8b9 | ||
|
|
de930b21a2 | ||
|
|
e4557b0473 | ||
|
|
b483335c06 | ||
|
|
b11e77e4c0 | ||
|
|
0157f7f96b | ||
|
|
21ec740d82 | ||
|
|
b085715654 |
No files matched your search
@@ -2,7 +2,9 @@
|
||||
/build/
|
||||
build-win/
|
||||
tools/chatgui/build/
|
||||
tools/chatgui/rlottie_build/
|
||||
tools/chatgui/ffmpeg/ffmpeg_build/
|
||||
tools/lightsout/build/
|
||||
|
||||
# Build outputs
|
||||
*.o
|
||||
@@ -11,6 +13,8 @@ tools/chatgui/ffmpeg/ffmpeg_build/
|
||||
*.a
|
||||
*.so
|
||||
*.so.*
|
||||
# Prebuilt native runtime для Android VAD (onnxruntime) — коммитим несмотря на *.so
|
||||
!tools/chatgui-android/app/src/main/jniLibs/*/libonnxruntime.so
|
||||
/utun
|
||||
/src/utun
|
||||
test_serialize
|
||||
@@ -28,6 +32,7 @@ index.html
|
||||
*.exe
|
||||
|
||||
# Autotools
|
||||
/.utun-build-state*
|
||||
Makefile
|
||||
!tools/etcpmon/Makefile
|
||||
Makefile.in
|
||||
@@ -80,6 +85,7 @@ logs/
|
||||
tests/test_*
|
||||
!tests/test_*.c
|
||||
!tests/test_*.h
|
||||
!tests/test_*.py
|
||||
!tests/test_*.conf
|
||||
!tests/test_*.json
|
||||
tests/bench_*
|
||||
@@ -108,6 +114,7 @@ tools/proxy/udp_proxy
|
||||
|
||||
# Reference projects (not part of utun)
|
||||
tools/tdesktop-dev/
|
||||
/hardware/
|
||||
|
||||
# Debug/temp artifacts
|
||||
*.bak
|
||||
|
||||
@@ -32,12 +32,17 @@ This file contains essential information for AI coding agents working in the uTu
|
||||
|
||||
Доствп на узел colo2: ssh -p44322 logs@colo2
|
||||
|
||||
Структура .h файлов:
|
||||
- в начале краткое и понятное описание: что делает модуль, как пользоваться.
|
||||
- поля структур также кратко описываем - что, для его, если есть нюансы/значения
|
||||
- для функций: описываем назначение, как пользоваться, описание аргументов, возврата. кратко но понятно.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Repository:** uTun - Secure VPN tunnel with ETCP protocol
|
||||
**Repository:** uTun — общее сетевое ядро ETCP, независимо запускаемые UTUN и P2P-чат
|
||||
**Language:** C (C99)
|
||||
**Build System:** GNU Autotools (autoconf/automake)
|
||||
**Cryptography:** OpenSSL (AES-CCM, X25519, SHA256)
|
||||
**Cryptography:** OpenSSL (AES-CCM, X25519, Ed25519, SHA256)
|
||||
|
||||
## Build Commands
|
||||
|
||||
@@ -51,9 +56,19 @@ This file contains essential information for AI coding agents working in the uTu
|
||||
powershell build_full.bat # запускает bash build.sh --full через MSYS2 UCRT64
|
||||
```
|
||||
|
||||
### Build (FreeBSD)
|
||||
```sh
|
||||
# Первичная установка инструментов от root:
|
||||
pkg install -y bash gmake autoconf automake libtool pkgconf openssl python3
|
||||
./build.sh -j4 # общий скрипт; автоматически выбирает gmake и Clang
|
||||
./build_bsd_full.sh -j4 # полная сборка через общий скрипт
|
||||
```
|
||||
Python 3 нужен для проверок скрипта сборки. SHA-256 берётся из базовой системы FreeBSD.
|
||||
|
||||
### Incremental Build
|
||||
```bash
|
||||
./build.sh -j4 # make с авто-конфигурацией если надо
|
||||
./build.sh -j4 # инкрементальная сборка, конфигурация и очистка при необходимости
|
||||
./build.sh --no-gui -j4 # только ядро и тесты, без отдельной сборки GUI
|
||||
```
|
||||
```powershell
|
||||
powershell -Command ".\build.bat" 2>&1 # Windows, логи: build_win.log
|
||||
@@ -68,9 +83,10 @@ make distclean # Clean everything including configure files
|
||||
|
||||
### Partial Builds
|
||||
```bash
|
||||
cd lib && make # Build only the library (libuasync.a)
|
||||
cd src && make # Build only the main program
|
||||
cd tests && make # Build only tests
|
||||
make -C lib # базовые библиотеки
|
||||
make -C lib/libopus # встроенный Opus
|
||||
make -C src # основной бинарник src/utun (библиотеки уже должны быть собраны)
|
||||
make -C tests # тесты (используют объекты src/)
|
||||
```
|
||||
|
||||
### Direct Build (Windows, без autotools)
|
||||
@@ -86,7 +102,7 @@ python3 tools/chatcli members <ch_id> # мемберы с полн
|
||||
python3 tools/chatcli messages <ch_id> [count] # последние сообщения
|
||||
python3 tools/chatcli send <ch_id> "text" # отправить сообщение
|
||||
python3 tools/chatcli invite <ch_id> # создать invite-ссылку
|
||||
python3 tools/chatcli connect <utun://...> # join по ссылке
|
||||
python3 tools/chatcli connect 'utun://...' # join по ссылке
|
||||
python3 tools/chatcli create "Name" # создать канал
|
||||
python3 tools/chatcli listen # слушать события (Ctrl+C выход)
|
||||
```
|
||||
@@ -94,13 +110,14 @@ python3 tools/chatcli listen # слушать событи
|
||||
|
||||
### ASAN Build (AddressSanitizer, Linux)
|
||||
```bash
|
||||
./build.sh --asan -j4 # lib/ + src/ only (tests not ASAN-compatible)
|
||||
./build.sh --asan -j4 # собирает lib/ + src/; тесты этот режим скрипта пропускает
|
||||
```
|
||||
Запуск с ASAN (detect_leaks отключены чтобы не спамить при shutdown):
|
||||
```bash
|
||||
ASAN_OPTIONS=detect_leaks=0:halt_on_error=0 ./src/utun -f -p /tmp/utun.pid -l /tmp/utun.log
|
||||
ASAN_OPTIONS=detect_leaks=0:halt_on_error=0:log_path=/tmp/utun_asan ./src/utun -f -p /tmp/utun.pid -l /tmp/utun.log
|
||||
```
|
||||
Краш-логи ASAN пишутся в `/tmp/utun_asan.<pid>`.
|
||||
В этом примере отчёты ASAN пишутся в `/tmp/utun_asan.<pid>` благодаря `log_path`.
|
||||
Для отдельного теста нужно собирать его зависимости с теми же sanitizer-флагами; обычные и ASAN-объекты не смешивать.
|
||||
|
||||
## Test Commands
|
||||
|
||||
@@ -118,13 +135,14 @@ cd tests/
|
||||
./test_etcp_two_instances
|
||||
```
|
||||
|
||||
### Run Single Test with Debug Info
|
||||
### Build and Run a Single Test
|
||||
```bash
|
||||
cd tests/
|
||||
gcc -I../src -I../lib -I../tinycrypt/lib/include \
|
||||
-o my_test test_file.c ../src/*.c ../lib/*.c ../tinycrypt/lib/source/*.c
|
||||
./my_test
|
||||
make -C tests test_services
|
||||
cd tests && ./test_services
|
||||
```
|
||||
Использовать цель из `tests/Makefile.am`: ручная компиляция `src/*.c` пропускает подкаталоги и добавляет лишний `main`.
|
||||
Уровни и категории диагностики настраиваются в самом тесте через `debug_set_level` / `debug_set_category_level`.
|
||||
Проверки самого скрипта сборки запускаются отдельно: `make -C tests check-build-script` (нужен Python 3); в `./check.sh` они не входят.
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
@@ -137,7 +155,7 @@ gcc -I../src -I../lib -I../tinycrypt/lib/include \
|
||||
|
||||
### Formatting
|
||||
- **Indentation:** 4 spaces, no tabs
|
||||
- **Braces:** Same line for functions, new line for control structures:
|
||||
- **Braces:** Открывающая скобка на строке функции или условия:
|
||||
```c
|
||||
int function(void) {
|
||||
if (condition) {
|
||||
@@ -145,7 +163,7 @@ gcc -I../src -I../lib -I../tinycrypt/lib/include \
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Comments:** Primary language is Russian for business logic, English for API docs
|
||||
- **Comments:** Краткие описания на русском; сохранять принятые имена API и протокольных полей.
|
||||
- **Line Length:** Aim for 80-100 characters, but can go up to 150 if logically coherent
|
||||
|
||||
### Include Order
|
||||
@@ -201,7 +219,7 @@ sc_context_t ctx;
|
||||
sc_init_ctx(&ctx, &my_keys);
|
||||
|
||||
// 2. Set peer public key (for key exchange)
|
||||
sc_set_peer_public_key(&ctx, peer_public_key, SC_PEER_PUBKEY_HEX); // 0=bin, 1=hex
|
||||
sc_set_peer_public_key(&ctx, peer_public_key_bin, SC_PEER_PUBKEY_BIN); // 32 бинарных байта; HEX — для hex-строки
|
||||
|
||||
// 3. Ready for encrypt/decrypt
|
||||
sc_encrypt(&ctx, plaintext, plaintext_len, ciphertext, &ciphertext_len);
|
||||
@@ -227,69 +245,107 @@ debug - отладка: всё что нужно для понимание су
|
||||
info - сообщения для пользователя о работе. мы должны видеть в читаемом виде понятные и осмысленные сообщения о каких-либо значимых действиях с точки зрения логики работы модуля или приложения
|
||||
warn / error - ошибки и предупреждения (аномалии). должны быть во всех ошибочных ветках. Молча вываливаться с ошибкой нельзя, все ошибки и предупреждения должны логироваться.
|
||||
|
||||
### Debug Categories (28 категорий)
|
||||
```
|
||||
NONE=0, UASYNC=1, LL_QUEUE=2, CONNECTION=3, ETCP=4, CRYPTO=5, MEMORY=6,
|
||||
TIMING=7, CONFIG=8, TUN=9, ROUTING=10, TIMERS=11, NORMALIZER=12, BGP=13,
|
||||
SOCKET=14, CONTROL=15, DUMP=16, TRAFFIC=17, DEBUG=18, GENERAL=19, NAT=20,
|
||||
KEEPALIVE=21, ETCPROUTE=22, BBR=23, ETCP_DUMP=24, CONNECTIVITY=25, DB_SYNC=26,
|
||||
MEMBER_SYNC=27
|
||||
```
|
||||
### Debug Categories
|
||||
Актуальные имена и константы — в `lib/debug_config.h`, текстовые имена — в `lib/debug_config.c`.
|
||||
Основные: `sys`, `connection`, `etcp`, `crypto`, `config`, `tun`, `routing`, `timers`, `bgp`,
|
||||
`socket`, `control`, `dump`, `traffic`, `debug`, `general`, `nat`, `keepalive`, `etcp_route`,
|
||||
`media`, `etcp_dump`, `chat`, `chat_sync`, `member_sync`, `proxy`, `video`, `reality`, `dm`, `call`, `radio`, `vad`, `aec`.
|
||||
UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в ETCP; старые числовые списки не использовать.
|
||||
|
||||
### Настройка отладки
|
||||
- В конфиге: `debug = etcp=trace,config=info` (формат: `категория=уровень,...`)
|
||||
- В INI-конфиге: секция `[debug]`, каждая категория отдельной строкой (`etcp=trace`, `config=info`).
|
||||
- Строку `etcp=trace,config=info` принимает `debug_parse_config()`; GUI хранит её в `[gui] debug_categories`.
|
||||
- В коде: глобальный уровень и per-category уровни из `debug_config_t g_debug_config`
|
||||
- Макросы: `DEBUG_ERROR(cat,fmt,...)` `DEBUG_WARN` `DEBUG_INFO` `DEBUG_DEBUG` `DEBUG_TRACE`
|
||||
- `log_dump(prefix, data, len)` — hex dump в лог
|
||||
- `log_dump(level, category, prefix, data, len)` — hex dump в лог
|
||||
|
||||
### Dual Output
|
||||
- Консоль и файл настраиваются раздельно (`debug_set_console_level`, `debug_set_file_level`)
|
||||
- Фильтр уровней общий для выходов; `debug_enable_console()` включает/выключает консоль.
|
||||
- `debug_enable_file_output(path, truncate)` / `debug_disable_file_output()`
|
||||
|
||||
## Architecture
|
||||
|
||||
### Ядро, сервисы и владение ресурсами
|
||||
|
||||
- `utun_instance_create*()` создаёт ядро; `utun_core_start()` запускает общие сетевые службы.
|
||||
- `utun_service_start/stop()` управляет UTUN-группой, TUN/NAT и запросами к пирам из конфига.
|
||||
- `chat_service_start/stop()` управляет чатом и CHAT-группами. Чат не требует UTUN; desktop и Android запускают ядро и чат явно.
|
||||
- Ядро владеет общей SQLite (`db_path/chats.db`, без пути — `:memory:`), идентичностью, транспортом и маршрутизатором.
|
||||
Остановка сервиса сохраняет БД и ресурсы другого сервиса. `utun_instance_destroy()` освобождает ядро, но не переданный `UASYNC`.
|
||||
- Группа владеет NCD handle с начала CONNECTING. Инициатор хранит отменяемый `TOPO_PEER_REQUEST`;
|
||||
закрытие своего запроса не разрывает транспорт других владельцев. Готовая сессия сохраняется у группы.
|
||||
- UP означает работоспособный транспорт. READY означает завершённое согласование и обмен таблицей конкретной группы.
|
||||
Recovery ждёт нужных маршрутов через READY-пиров; наличие `ETCP_CONN*` само по себе не гарантирует UP.
|
||||
- JOIN добавляет подписанного мембера, Merkle распространяет запись. Доступ к CHAT-группе каждый узел даёт по своей таблице участников;
|
||||
invite-ключ не даёт временного членства. Протокол описан в `src/chat/chat_join.h`, групповое согласование — в `src/routing_layer/topo_group.h`.
|
||||
- `TOPO_NODE` — общая подписанная запись узла. Более свежий timestamp заменяет запись целиком, с полями и подписью;
|
||||
адреса не версионируются отдельно. BGP обновляет запись и передаёт изменения NCD (см. `doc/node_snapshot.md`).
|
||||
- BGP хранит один путь к назначению от каждого соседа. WITHDRAW удаляет только путь отправителя;
|
||||
NODEINFO с прежним timestamp также обновляет путь. Выбор: живой путь с минимумом хопов, затем меньший ID соседа.
|
||||
REINIT сохраняет пути до проверки нового снимка; TABLE_COMPLETE удаляет неподтверждённые, зависший обмен перезапускается.
|
||||
- ETCP-router дописывает ID при транзите и отбрасывает повторное посещение для DATA/ACK/control.
|
||||
Список переходов находится вне подписанного тела; формат описан в `src/routing_layer/etcp_router.h`.
|
||||
|
||||
Все операции с ядром выполняются в его uasync-потоке; stop/destroy — вне callbacks останавливаемых сервисов.
|
||||
Порядок освобождения ресурсов и нюансы media workers: `doc/service_lifecycle.md`.
|
||||
|
||||
### Directory Structure
|
||||
```
|
||||
├── lib/ # Core libraries (18 .c + 20 .h + libopus/liblmdb)
|
||||
├── lib/ # uasync, очереди, SQLite, Opus, SpeexDSP, Silero VAD, SoundTouch, LMDB
|
||||
├── src/ # Main source code
|
||||
│ ├── transport_layer/ # ETCP stack, crypto, STCP, normalizer, BBR (~30 файлов)
|
||||
│ ├── routing_layer/ # Routing, BGP, conn_mgr, etcp_router, NAT detection (~20 файлов)
|
||||
│ ├── chat/ # P2P чат, db_sync, merkle_sync, member_sync (17 файлов)
|
||||
│ ├── proxy/ # SOCKS5, TCP/UDP/ICMP прокси (10 файлов)
|
||||
│ ├── lwip_tcp/ # Встроенный lwIP TCP стек (7 файлов)
|
||||
│ ├── media_delivery/ # Доставка медиафайлов (6 файлов)
|
||||
│ ├── media_async/ # Асинхронный движок медиа (2 файла)
|
||||
│ └── *.c/h # Корневые модули: utun, config, TUN, firewall, NAT, NTP, control (~14 файлов)
|
||||
├── tests/ # ~70 тестов, 64 в check_PROGRAMS
|
||||
│ ├── transport_layer/ # ETCP, сессии, crypto, STCP/REALITY, SOCKS-клиент, NCD, BBR
|
||||
│ ├── routing_layer/ # Routing, группы/BGP, recovery, conn_mgr, etcp_router
|
||||
│ ├── chat/ # P2P чат, join, db_sync, merkle_sync, member_sync
|
||||
│ ├── dm/ # Личные сообщения, E2E-вложения и offline-mailbox
|
||||
│ ├── call/ # Звонки и общий голосовой стек
|
||||
│ ├── radio/ # Групповая PTT-рация
|
||||
│ ├── stream/ # Прямые трансляции: подписанные RS-блоки, выбор поставщиков, аудио/видео
|
||||
│ ├── member_files/ # Файловое хранилище участников CHAT-группы и асинхронные передачи
|
||||
│ ├── local_transport/ # Обнаружение устройств и доставка приглашений в локальной сети по UDP
|
||||
│ ├── proxy/ # SOCKS5, TCP/UDP/ICMP прокси
|
||||
│ ├── lwip_tcp/ # Встроенный lwIP TCP стек
|
||||
│ ├── media_delivery/ # Блоки медиа каналов и передача неизменяемых файлов
|
||||
│ ├── media_async/ # Фоновые задачи, метаданные вложений, подготовка голоса/видео
|
||||
│ ├── video/ # Пробинг и транскодирование видео через FFmpeg
|
||||
│ ├── dnsmasq/ # Архив исходников для встроенной сборки dnsmasq
|
||||
│ └── *.c/h # utun, instance, config, TUN, firewall, NAT, NTP, control, broadcast
|
||||
├── tests/ # Состав тестов и цели запуска: tests/Makefile.am
|
||||
├── doc/ # Technical Specifications
|
||||
├── tools/ # Auxiliary tools
|
||||
│ ├── etcpmon/ # GUI монитор ETCP
|
||||
│ ├── chatgui/ # GUI чат (Qt 6 с Qt 5 fallback, сборка CMake)
|
||||
│ ├── chatgui-android/ # Android P2P чат (Jetpack Compose + libutun_lite)
|
||||
│ ├── logreceiver/ # Приём UDP-логов Android
|
||||
│ ├── chat_tcp_test/ # Сценарий проверки чата через TCP
|
||||
│ ├── lightsout/ # Отдельная игра Lights Out на Qt
|
||||
│ ├── lightsout-android/ # Android-версия Lights Out
|
||||
│ ├── tdesktop-dev/ # Референс: Telegram Desktop (только для изучения)
|
||||
│ ├── proxy/ # UDP прокси для тестов
|
||||
│ └── bping/ # BPing (bandwidth ping)
|
||||
│ ├── bping/ # BPing (bandwidth ping)
|
||||
│ └── chatcli # Headless CLI; описание команд — chatcli_commands.txt
|
||||
├── tinycrypt/ # TinyCrypt crypto library (external)
|
||||
├── net_emulator/ # Network emulator (delays, loss, reordering)
|
||||
└── c2/ # Test instance 2 (конфиг и бинарник для тестов)
|
||||
└── net_emulator/ # Network emulator (delays, loss, reordering)
|
||||
```
|
||||
|
||||
### File Overview
|
||||
|
||||
Пути ниже относительны к каталогу секции; `name.c/h` означает пару `.c` и `.h`.
|
||||
Перечислены модули проекта; внутренние файлы сторонних библиотек сгруппированы по каталогам.
|
||||
|
||||
**Core (src/)**
|
||||
- `utun.c` - Main program entry point, CLI parsing, daemon mode
|
||||
- `utun_instance.c/h` - Root instance lifecycle, config loading, all submodule init
|
||||
- `utun_instance.c/h` - Создание ядра и независимый жизненный цикл UTUN/чата, владение общими ресурсами
|
||||
- `tun_if.c/h` - TUN interface API (init/write/close, cross-platform)
|
||||
- `tun_linux.c` `tun_freebsd.c` `tun_windows.c` - Platform-specific TUN implementations
|
||||
- `tun_route.c/h` - TUN routing table sync
|
||||
- `config_parser.c/h` - INI-style config file parsing
|
||||
- `config_updater.c/h` - Config file modification utilities
|
||||
- `config_updater.c/h` - Проверка и сохранение согласованных X25519-ключей и node_id в INI-конфиге
|
||||
- `control_server.c/h` - Control/monitoring server (etcpmon backend API)
|
||||
- `firewall.c/h` - Firewall rules (per-interface filtering)
|
||||
- `eim_nat.c/h` - Endpoint-Independent Mapping NAT engine
|
||||
- `nat_transport.c/h` - NAT transport layer (packet relay)
|
||||
- `ntp_time.c/h` - NTP time synchronization
|
||||
- `ntp_node_time.c/h` - Inter-node time synchronization
|
||||
- `nat_transport.c/h` - Передача IPv4 между отдельным TUN и удалённым NAT-провайдером через UTUN-router; локальный провайдер использует eim_nat
|
||||
- `ntp_time.c/h` - Асинхронная NTP-синхронизация: поправка времени instance без изменения часов ОС
|
||||
- `ntp_node_time.c/h` - Получение временной поправки от синхронизированных ETCP-пиров и диагностика расхождения часов
|
||||
- `broadcast.c/h` - Широковещательная рассылка данных по topo-группе (UUID-дедупликация, TTL)
|
||||
- `video/video.c/h` - Пробинг и транскодирование видео (FFmpeg, опционально; H.264/MP4 ≤720p)
|
||||
- `dnsmasq/` - Архив dnsmasq-2.93.tar.xz для встроенной сборки
|
||||
@@ -298,74 +354,142 @@ MEMBER_SYNC=27
|
||||
- `etcp.c/h` - ETCP protocol implementation (inflight queues, retrans, ACK, RTT/jitter)
|
||||
- `etcp_api.c/h` - ETCP public API (send/recv/bind callbacks)
|
||||
- `etcp_connections.c/h` - Socket and link management, INIT handshake, keepalive, PING/PONG
|
||||
- `etcp_session.c/h` - Согласование эпох ETCP-сессии через HELLO/CHALLENGE/CONFIRM и проверка принадлежности DATA сессии
|
||||
- `etcp_loadbalancer.c/h` - Multi-link load balancing with traffic shaper
|
||||
- `etcp_debug.c/h` - ETCP packet dump/formatting
|
||||
- `etcp_debug.c/h` - Диагностический разбор секций ETCP_DGRAM и форматирование IPv4-адресов
|
||||
- `etcp_dump.c/h` - ETCP дамп/декодирование пакетов
|
||||
- `etcp_bbr.c/h` - BBR congestion control for ETCP
|
||||
- `etcp_connect.c/h` - ETCP outbound connections
|
||||
- `etcp_keepalive.c/h` - Keepalive-пакеты, адаптивный период, смена режима standby/normal (мобильные узлы)
|
||||
- `auto_socket.c/h` - Автосоздание UDP+TCP сокетов на всех интерфейсах и линков к ETCP-соединениям (`auto_sockets=yes`)
|
||||
- `pkt_normalizer.c/h` - Packet fragmentation/reassembly (packer/unpacker)
|
||||
- `packet_dump.c/h` - Packet hex dump utility
|
||||
- `dummynet.c/h` - Network emulator integrated into utun (for testing)
|
||||
- `secure_channel.c/h` - AES-CCM encryption with X25519 key exchange, pubkey obfuscation
|
||||
- `packet_dump.c/h` - Текстовая сводка IPv4-пакета: адреса, поля транспорта и короткий hex-фрагмент
|
||||
- `dummynet.c/h` - Двунаправленный UDP-эмулятор между заданными endpoint: задержка, ограничение скорости и случайные потери
|
||||
- `secure_channel.c/h` - X25519, пакетное AES-128-CCM, потоковое AES-CTR, подписи Ed25519 и обфускация публичного ключа
|
||||
- `etcp_padding.c/h` - Выбор длины случайного padding и периодическое обновление диапазона для instance/соединения
|
||||
- `etcp_tx_stats.c/h` - Общая статистика исходящих очередей, inflight и подтверждённых за секунду байтов ядра
|
||||
- `crc32.c/h` - CRC32 checksums
|
||||
- `stcp.c/h` - STCP протокол (Secure TCP)
|
||||
- `stcp_server.c/h` - STCP сервер
|
||||
- `stcp_client.c/h` - STCP клиент
|
||||
- `stcp_link.c/h` - STCP линк-уровень
|
||||
- `node_conn_direct.c/h` - Direct config-based connections
|
||||
- `socket_monitor.c/h` - Socket state monitoring
|
||||
- `stcp.c/h` - Зашифрованные кадры поверх TCP: согласование ключей/эпох, AES-CTR, CRC, очереди и закрытие соединения
|
||||
- `stcp_server.c/h` - Приём входящих STCP-соединений, handshake и передача готового соединения владельцу
|
||||
- `stcp_client.c/h` - Исходящее STCP-подключение, в том числе через SOCKS5 и REALITY
|
||||
- `stcp_link.c/h` - Подключение STCP к ETCP_LINK: передача пакетов, backpressure и жизненный цикл TCP-линка
|
||||
- `socks_client.c/h` - Исходящий SOCKS5-транспорт: TCP CONNECT и UDP ASSOCIATE с переподключением control-сокета
|
||||
- `node_conn_direct.c/h` - Общий прямой транспорт к пиру с отдельным handle каждого владельца; обновление линков по адресам узла
|
||||
- `socket_monitor.c/h` - Наблюдение за адресами интерфейсов и маршрутами ОС, обновление сетевого состояния ETCP-сокетов
|
||||
- `reality.c/h` - REALITY-style TLS ClientHello/ServerHello камуфляж для STCP
|
||||
- `reality_fingerprint.c/h` - Статические отпечатки (cipher suites, groups, ALPN) для REALITY-камуфляжа
|
||||
- `reality_io.c/h` - Внешний REALITY-handshake и поток защищённых записей STCP, Finished, replay-защита и выбор relay
|
||||
- `reality_record.c/h` - TLS-подобные AES-128-GCM записи: вывод ключей, счётчики направлений, padding и KeyUpdate
|
||||
- `reality_relay.c/h` - Релей неавторизованных клиентов на реальный HTTPS-сайт
|
||||
- `stun_nat.c/h` - Проверка mapping/filtering NAT через внешние STUN-серверы, RFC 5780 и кеш результатов по IP/интерфейсу
|
||||
- `call_ring.c/h` - Кольцевой трейс контрольных точек освобождения ETCP/STCP-ресурсов для диагностики teardown/UAF
|
||||
|
||||
**BBR (src/transport_layer/BBR/)**
|
||||
- `bbr_v3.c` - BBR congestion control algorithm v3
|
||||
|
||||
**Routing Layer (src/routing_layer/)**
|
||||
- `routing.c/h` - Routing table (local)
|
||||
- `route_lib.c/h` - Routing library
|
||||
- `route6_lib.c/h` - IPv6 routing
|
||||
- `route_ping.c/h` - Route ping probing (NAT check, liveness)
|
||||
- `route_connectivity.c/h` - Route connectivity checks
|
||||
- `topo_group.c/h` - BGP-like route exchange between nodes (NODEINFO/WITHDRAW, изолированные группы UTUN/CHAT)
|
||||
- `topo_group_connect.c/h` - Авто-подключение к узлам CHAT-группы (3 фазы, бесконечный цикл)
|
||||
- `topo_group_invite.c/h` - Invite/join канала через прямое ncd-подключение (INVITE_INFO_REQ/RESP)
|
||||
- `topo_node.c/h` - Node management (модель узла: TOPO_NODE/TOPO_GROUP_NODE, сериализация, node_registry)
|
||||
- `topo_node_sqlite.c/h` - SQLite persistence for nodes
|
||||
- `topo_recovery.c/h` - Восстановление каскадно отвалившихся узлов после разрыва соединения
|
||||
- `routing.c/h` - Обмен IPv4-пакетами UTUN между локальным TUN и сервисом DATA ETCP-router
|
||||
- `route_lib.c/h` - Локальная таблица IPv4-подсетей UTUN: добавление/удаление маршрутов и поиск узла назначения
|
||||
- `route6_lib.c/h` - Локальная таблица IPv6-подсетей на radix tree с поиском по самому длинному префиксу
|
||||
- `route_ping.c/h` - Пинг цели через явно выбранного BGP-посредника для NAT-проверки
|
||||
- `route_connectivity.c/h` - Прямые UDP/TCP-пробы адресов узла для оценки достижимости и RTT при выборе пиров
|
||||
- `topo_group.c/h` - Сессии UTUN/CHAT (JOIN/ACCEPT, эпохи, READY), групповые запросы, NODEINFO/WITHDRAW и владение NCD
|
||||
- `topo_group_connect.c/h` - Подбор пиров CHAT-группы по роли: суперузлы → desktop → mobile; история/RTT внутри класса, backoff и standby burst
|
||||
- `topo_group_invite.c/h` - Получение описания канала через INVITE_INFO_REQ/RESP; добавление мембера реализовано в chat_join/chat_sync
|
||||
- `topo_node.c/h` - Модель TOPO_NODE/TOPO_GROUP_NODE, подписанные снимки узлов, сериализация и общий node_registry
|
||||
- `topo_node_sqlite.c/h` - Хранение подписанных снимков узлов, адресов, каналов и блоков мемберов в общей SQLite
|
||||
- `topo_recovery.c/h` - Восстановление потерянных маршрутов группы через READY-сессии, отмена попыток при stop
|
||||
- `conn_mgr.h` + `conn_mgr_core.c`/`conn_mgr_indirect.c`/`conn_mgr_monitor.c` + `conn_mgr_priv.h` — Connection Manager (handle-based API, 3-phase DIR/REV/IND)
|
||||
- `etcp_router.c/h` - ETCP router/multiplexer (мультиплексирование каналов, transit forwarding)
|
||||
- `nat_detection.c/h` - NAT type detection
|
||||
- `etcp_router.c/h` - Надёжные логические каналы сервисов поверх BGP-маршрутов: ACK, окна, транзит и защита от циклов
|
||||
- `route_tx_stats.c/h` - Скользящее окно подтверждённых payload-байтов логического канала для оценки скорости доставки
|
||||
- `multicast.c/h` - Рассылка списку узлов группы с разделением маршрута на ветви, локальными подписками и отчётами о потерях
|
||||
- `nat_detection.c/h` - Проверка mapping и доступности входящих рабочих сокетов через ETCP-пиров с учётом предварительных STUN-измерений
|
||||
- `address_profile.h` - Профиль endpoint: изменение адреса, доступность входящих и источник измерения ETCP/STUN
|
||||
- `sock_match.c/h` - Классификация локальных сокетов и адресов пира, отбор совместимых пар для прямого подключения
|
||||
- `route_crypto.c/h` - Пред/пост-обработка SVC_ROUTE пакетов (encrypt-then-sign)
|
||||
|
||||
**Chat (src/chat/)** — децентрализованный P2P чат: сообщения и участники автоматически синхронизируются между всеми узлами канала без центрального сервера.
|
||||
- `chat_core.c/h` - Главный модуль чата. Единственная точка входа из GUI: принять сообщение, создать канал, изменить настройку. Управляет БД и жизненным циклом всех chat-модулей
|
||||
- `chat_core.c/h` - API сообщений, каналов, профиля и настроек; chat_service_start/stop управляют чат-сервисом поверх общей БД ядра
|
||||
- `chat_core_priv.h` - Внутренний API для частей chat_core: общий контекст и утилиты БД. Снаружи не используется
|
||||
- `chat_sync.c/h` - Связывает чат с ETCP-сетью: отслеживает появление/разрыв соединений, обновляет онлайн-статус узлов, запускает синхронизацию участников каналов, обрабатывает приглашения
|
||||
- `chat_event.c/h` - Доставка событий из ядра чата в GUI. Единый механизм: любой модуль отправляет событие (новое сообщение, смена участников, статус), GUI получает через один обработчик
|
||||
- `chat_setting.c/h` - Настройки чата: проверяет корректность значений, хранит текущие. Используется и headless-парсером конфига, и GUI. Не зависит от других модулей
|
||||
- `db_sync.c/h` - Автоматическая репликация данных между подключёнными узлами. Запись на одном узле — появляется у всех остальных. Криптоподписи и цепочка хешей защищают от подделок
|
||||
- `chat_setting.c/h` - Проверка и хранение настроек чата per-instance; общая логика для конфигурации и GUI
|
||||
- `chat_join.c/h` - Регистрация invite-ключа с ACK, пересылка запроса инвайтеру и добавление подписанного мембера; контракт join в заголовке
|
||||
- `db_sync.c/h` - Репликация подписанных записей SQLite через прямые ETCP-соединения; расхождения по цепочке хешей, новые записи через PUSH
|
||||
- `member_sync.c/h` - Синхронизация участников каналов поверх merkle_sync. Два подписанных блока (мембер/владелец), по-блочное сравнение версий, верификация подписей. Удаление — только битая запись
|
||||
- `merkle_sync.c/h` - Универсальный движок синхронизации на Merkle-деревьях (5 уровней × 32 бакета, SHA256). Пиры обмениваются хешами и передают только различия; per-session out-очередь с backpressure. Не привязан к конкретному типу данных (коллбэки `data_ops`)
|
||||
- `chat_profile.c` - Собственный профиль: имя узла, сетевые адреса, сохранение UI-состояния (часть chat_core)
|
||||
- `chat_channel.c` - Создание и настройка каналов: готовит таблицы в БД, генерирует криптоключи, запускает подключение ко всем участникам (часть chat_core)
|
||||
- `merkle_tree.c/h` - Производный индекс Merkle-хешей в SQLite; восстанавливается из данных модели, обновляется с ними в одной транзакции
|
||||
- `chat_profile.c` - Подписанный профиль мембера: имя, аватар и опции сервисов; локальное UI-состояние (часть chat_core)
|
||||
- `chat_channel.c` - Создание и настройка каналов: таблицы, криптоключи, CHAT-группа и планировщик подключения (часть chat_core)
|
||||
- `chat_msg.c` - Отправка и приём сообщений: запись в локальную БД, автоматическая рассылка всем участникам канала (часть chat_core)
|
||||
- `chat_feedback.c/h` - Подписанные реакции и ответы: версии состояния, сохранение отмен и Merkle-синхронизация между участниками
|
||||
- `chat_poll.c/h` - Создание опросов, голосование, реакции и JSON-снимки результатов для UI/headless
|
||||
- `chat_poll_codec.c` - Проверка и wire-кодеки опросов, ответов и реакций; общий каталог эмодзи (API в chat_poll.h)
|
||||
- `chat_attachment.c/h` - Зашифрованные вложения канала: ключи, запросы доступа, ручное одобрение автора и сохраняемый outbox
|
||||
- `chat_local.c/h` - Обнаружение устройств чата и обмен готовыми invite-ссылками через локальный UDP-транспорт
|
||||
- `group_avatar.c/h` - Подписанная аватарка канала: хранение версии в SQLite, проверка подписи и передача блоков через router
|
||||
- `chat_status.c` - Сбор диагностики: текущее время, активные соединения, типы NAT. Отправляется в GUI (часть chat_core)
|
||||
- `chat_member.c/h` - Единая структура мембера для отображения в GUI (десктоп/headless/Android), фиксированная сериализация на проводе
|
||||
- `chat_admin.c/h` - Передача канального приватного ключа (прав админа) другому узлу
|
||||
- `chat_whisper.c/h` - Whisper speech-to-text транскрипция голосовых сообщений (опционально, асинхронно через media_async)
|
||||
- `chat_headless_control.c/h` - TCP control-socket для headless chat CLI (JSON line protocol)
|
||||
- `invite_build.c/h` - Сборка invite-ссылок utun:// (выбор лучшего узла + адреса)
|
||||
- `invite_link.c/h` - Формат invite-ссылок utun:// для каналов (base64: version|password|channel_id|pubkey|addrs)
|
||||
- `invite_link.c/h` - Кодирование utun://: версия, пароль, channel_id, join_key, Reality-параметры, ключ и адреса узла
|
||||
|
||||
**Личные сообщения (src/dm/)**
|
||||
|
||||
- `dm_core.c/h` - Беседы двух узлов, подписанные E2E-сообщения, SQLite/outbox, доставка через router/mailbox и квитанции
|
||||
- `dm_crypto.c/h` - Вывод ID беседы и ключа X25519, AES-256-CCM сообщений и E2E-контейнеры медиа через media_crypto
|
||||
- `dm_history.c/h` - Локальное удаление истории и скрытие бесед; поколения задач и отпечатки удалённых сообщений против повторов
|
||||
- `dm_mailbox.c/h` - Offline-очередь суперузла: хранение подписанных сообщений и повтор доставки до квитанции получателя
|
||||
- `dm_media.c/h` - Подготовка, доставка и публикация E2E-вложений; ciphertext-хранилище и отдельные квитанции файлов
|
||||
- `dm_media_priv.h` - Внутренние SQL/control-операции для dm_media и dm_mailbox_media
|
||||
- `dm_mailbox_media.c/h` - Сохраняемые задания суперузла для размещения медиа, доставки метаданных и удаления копий до DELETE_ACK
|
||||
|
||||
**Звонки (src/call/)**
|
||||
|
||||
- `call.c/h` - P2P аудио/видеозвонок: подписанный E2E-сигналинг, состояния, политика занятости и оптимизация пути через conn_mgr
|
||||
- `call_priv.h` - Общее внутреннее состояние сессии для call.c и call_video_transport.c; доступ только из uasync
|
||||
- `call_proto.h` - Wire-формат сигналинга, аудиокадров и управления видеотреком; треки/кодеки и причины завершения
|
||||
- `call_audio.c/h` - Общий аудио-движок: кодирование/декодирование Opus, jitter-буфер и тоны, API feed/pull PCM
|
||||
- `call_jitter.cpp/h` - FIFO Opus, адаптивный сетевой запас, декодирование на pull и восстановление воспроизведения после пауз
|
||||
- `call_jitter_window.h` - Окно из десяти секундных интервалов для оценки глубины очереди и изменения задержки доставки
|
||||
- `call_clock.h` - Метки времени захвата и снимок реально проигранного аудио для синхронизации видеотрека
|
||||
- `call_delay.cpp/h` - Уменьшение накопленной задержки PCM прямым ускорением и SoundTouch с сохранением исходной шкалы времени
|
||||
- `call_timeline.cpp/h` - Карта исходного PTS через обработку SoundTouch до позиции аудиоустройства
|
||||
- `call_video.c/h` - Общий формат H.264/H.265 видеотрека, согласование возможностей и сборка кадров из фрагментов
|
||||
- `call_video_bitstream.c/h` - Проверка конфигурации и IDR без reorder, профиля/уровня и геометрии H.264/H.265; KEY-пробы кодеков
|
||||
- `call_video_transport.c/h` - Отдельные router-каналы control/audio/video, backpressure, привязка видео к аудио и запросы KEY
|
||||
- `call_video_rate.c/h` - Регулятор битрейта и допуска новых видеокадров по скорости доставки, RTT и долгу очереди
|
||||
- `call_video_port.c/h` - Потокобезопасный обмен кадрами и командами между платформенными video workers и uasync-транспортом
|
||||
- `call_video_playout.c/h` - Расписание показа по аудиочасам, ожидание/отбрасывание кадров и подтверждение фактического показа
|
||||
- `call_video_fixtures.h` - Встроенные H.264/H.265 KEY-пробы для проверки базовых режимов кодека
|
||||
- `call_video_fast_fixtures.h` - KEY-пробы видеорежимов с частотой 60 fps
|
||||
- `call_video_fullhd_fixtures.h` - KEY-пробы видеорежимов Full HD
|
||||
- `call_video_portrait_fixtures.h` - KEY-пробы видеорежимов в портретной ориентации
|
||||
- `call_tones.c/h` - Генерация PCM-тонов паузы и завершения звонка
|
||||
- `call_headless.c/h` - Отдельный TCP-аудиосокет headless-клиента: обмен PCM; сигналинг остаётся на control-сокете
|
||||
|
||||
**Рация (src/radio/)**
|
||||
|
||||
- `radio.c/h` - Передача Opus по CHAT-группе, подписки, дедупликация и пересылка по ветвям с подписчиками
|
||||
- `radio_proto.h` - Wire-формат PTT: источник, поток, seq, FIN и hop-list против циклов
|
||||
- `radio_audio.c/h` - Общий аудио-движок: независимые RX/TX, Opus, микширование источников, ручной PTT и VAD-автомат
|
||||
- `radio_jitter.cpp/h` - Буфер кодированных кадров каждого источника с pre-roll и SoundTouch time-stretch на pull
|
||||
- `radio_vad.h` - Ресемплинг 48→16 кГц и автомат авто-PTT с подтверждением речи, задержкой завершения и учётом занятого канала
|
||||
- `radio_auto.h` - Общий автомат автопередачи по VAD/уровню звука, без захвата аудио и сетевого I/O
|
||||
- `radio_call.c/h` - Адресное приглашение участника в рацию канала: сигналинг вызова, занятость, принятие и отмена
|
||||
- `radio_headless.c/h` - TCP-аудиосокет headless-рации: PCM и команды начала/конца PTT
|
||||
|
||||
**Прокси (src/proxy/)**
|
||||
- `socks_proxy.c/h` - SOCKS5 прокси
|
||||
- `udp_proxy.c/h` - UDP прокси
|
||||
- `icmp_proxy.c/h` - ICMP прокси
|
||||
- `tcp_proxy_server.c/h` - TCP прокси (серверная сторона)
|
||||
- `tcp_proxy_client.c/h` - TCP прокси (клиентская сторона)
|
||||
|
||||
- `proxy_protocol.h` - Общий TCP-протокол и управление потоком: CONNECTED, DATA, кредит WINDOW, упорядоченный FIN
|
||||
- `socks_proxy.c/h` - Клиентские SOCKS5 CONNECT и HTTP proxy/CONNECT, DNS и передача потока выбранному exit-узлу
|
||||
- `http_proxy.c/h` - Потоковый HTTP/1.x кодек: проверка заголовков, разбор тела/chunked и учёт потреблённых байтов для WINDOW
|
||||
- `udp_proxy.c/h` - Передача UDP из TUN через exit-узел, сопоставление ответов и удаление неактивных потоков
|
||||
- `icmp_proxy.c/h` - Проксирование IPv4 ICMP echo через raw socket exit-узла с восстановлением исходных id/seq
|
||||
- `tcp_proxy_server.c/h` - Exit-сторона TCP-прокси: реальные исходящие сокеты, потоки по (peer, stream_id), окна и half-close
|
||||
- `tcp_proxy_client.c/h` - Клиентская сторона TCP-прокси: адаптер TUN/lwIP и обмен с настроенным exit через ETCP-router
|
||||
|
||||
**lwIP TCP (src/lwip_tcp/)**
|
||||
- `lwip_tcp.c/h` - lwIP TCP стек
|
||||
@@ -374,31 +498,80 @@ MEMBER_SYNC=27
|
||||
- `lwip_tcp_priv.h`, `lwip_tcp_opts.h` - Внутренние настройки lwIP
|
||||
|
||||
**Media Delivery (src/media_delivery/)**
|
||||
- `media_delivery.c/h` + `media_delivery_proto.h` - Доставка медиафайлов, протокол
|
||||
- `media_download.c/h` - Скачивание медиа
|
||||
- `media_index.c/h` - Индексация медиафайлов
|
||||
- `media_delivery.c/h` - Доставка блоков медиа каналов, учёт держателей, репликация доступности между суперузлами и relay
|
||||
- `media_delivery_proto.h` - Кодограммы канального медиа: QUERY, BLOCK_REQ/CHUNK/DONE, HAVE_BLOCK и SUPER_REPL
|
||||
- `media_download.c/h` - Загрузка блоков с выбором держателей, failover, контролем простоя, проверкой подписей и сборкой файла
|
||||
- `media_index.c/h` - Индекс media_files: UUID, хеши и подписи блоков, асинхронная регистрация и удаление медиа канала
|
||||
- `file_transfer.c/h` - Передача неизменяемого файла между заданными узлами; авторизация lookup и владение CM handle до завершения
|
||||
|
||||
**Media Async (src/media_async/)**
|
||||
- `media_async.c/h` - Асинхронный движок (thread-per-task для crypto/file операций)
|
||||
- `media_async.c/h` - Фоновые crypto/file-задачи: work в pthread, done в uasync; destroy ждёт workers и завершает ожидающие callbacks с CANCELLED
|
||||
- `attachment.c/h` - Общие метаданные file/voice/video, проверка и компактная wire-сериализация
|
||||
- `voice_file.c/h` - Worker-кодирование PCM в контейнер Opus с длительностью и waveform для desktop/Android
|
||||
- `attachment_send.c/h` - Асинхронная подготовка голоса/видео и отправка в зафиксированный канал или DM
|
||||
- `media_crypto.c/h` - Общие AES-256-CCM примитивы и порционное шифрование/проверка файлов для DM и групповых вложений
|
||||
- `encrypted_file.c/h` - Контейнер группового вложения: шифрование метаданных/файла, проверка SHA256 и durable-публикация результата
|
||||
|
||||
**Трансляции (src/stream/)** — live-аудио/видео CHAT-группы с подписью источника, Reed–Solomon и раздачей через участников.
|
||||
|
||||
- `stream.c/h` - Жизненный цикл трансляции, локальные handles издателя/читателей, события и публикация подписок
|
||||
- `stream_priv.h` - Внутренний доступ к сессиям, приём проверенных DATA/END и взаимодействие с планировщиком relay
|
||||
- `stream_proto.h` - Форматы подписанной пачки и RS-частей, дескриптор источника, размеры и коды результата
|
||||
- `stream_block.c/h` - Подпись пачки Ed25519 до RS-кодирования, разбор частей и проверка восстановленных данных
|
||||
- `stream_buffer.c/h` - Ограниченное кольцо RS-частей: дедупликация, восстановление пачек, проверка подписи и вытеснение старых данных
|
||||
- `stream_source.c/h` - Упаковка исходных байтов в подписанные RS-пачки, сброс неполной пачки по таймеру и завершение источника
|
||||
- `stream_member.c/h` - Объявления источников и подписок в опциях подписанного мембера; разбор и проверка принадлежности источника
|
||||
- `stream_wire.c/h` - Протокол выдач и relay: кодограммы управления, пути, метрики и подписанное завершение END
|
||||
- `stream_network.c/h` - Выдача блоков через прямой ETCP, NCD-владение, управляющие очереди, аренда подписок и reverse-запросы
|
||||
- `stream_optimizer.c/h` - Автоматический выбор и перенос поставщиков субпотоков, пробы доступности и привлечение relay
|
||||
- `stream_metrics.c/h` - Оценка относительного отставания субпотока и истории проблем по часам источника без NTP
|
||||
- `stream_load.c/h` - Учёт логической очереди DATA и её роста для обнаружения перегрузки раздающего узла
|
||||
- `stream_media.c/h` - Формат uAV2: упаковка/проверка видеокадров, конфигурации кодека, PTS и Opus внутри FEC-пачек
|
||||
- `stream_media_buffer.c/h` - Медиаиндекс общего RS-кольца и независимые курсоры audio/video; сборка кадров и восстановление с KEY
|
||||
- `stream_publisher.c/h` - Общий потокобезопасный publisher desktop/Android: очередь готовых кадров, публикация и завершение с FIN
|
||||
- `stream_viewer.c/h` - Независимые viewers: запрос ограниченных батчей по бюджетам треков, уведомления готовности и конца audio/video
|
||||
- `stream_playback_audio.cpp/h` - Opus/SoundTouch и очередь готового PCM; ускорение без изменения высоты и привязка к позиции устройства
|
||||
- `stream_playback_clock.c/h` - Общие часы воспроизведения и плавный догон live-потока по непрерывному запасу готового материала
|
||||
|
||||
**Файлы участников (src/member_files/)**
|
||||
|
||||
- `member_files.c/h` - Сервер и асинхронный клиент хранилища CHAT-группы: права доступа, запросы и события через защищённый router
|
||||
- `protocol.c/h` - Wire-кодек файловых команд, снимки версий каталогов/файлов и токены временных загрузок
|
||||
- `storage.c/h` - Серверные каталоги участников, квоты, файловые операции, временные загрузки и их очистка
|
||||
- `transfer.c` - Передача файлов с CM handle, backpressure, дисковым worker, проверкой хеша и публикацией результата
|
||||
- `disk.c/h` - Дисковые примитивы POSIX/Windows: безопасное разрешение путей без ссылок и I/O по 64-битному смещению
|
||||
- `worker.c/h` - Единственный дисковый pthread с ограниченной очередью и возвратом результатов в uasync
|
||||
- `private.h` - Внутреннее состояние запросов, передач, серверного хранилища и дискового worker
|
||||
|
||||
**Локальная сеть (src/local_transport/)**
|
||||
|
||||
- `local_transport.c/h` - Открытый UDP-транспорт LAN: обнаружение устройств, доставка сообщений с ACK/повторами и ограничения входящего потока
|
||||
- `platform.c/h` - Снимок IPv4/IPv6 интерфейсов, broadcast/multicast-адреса, приём метаданных UDP и проверка локальности источника
|
||||
- `protocol.h` - Wire-формат объявлений, сообщений и ACK: ID/эпохи устройств, ID запроса и проверка UTF-8 имени
|
||||
|
||||
**Libraries (lib/)**
|
||||
- `u_async.c/h` - Async event loop (epoll/poll/select, timers via timeout_heap)
|
||||
- `ll_queue.c/h` - Lock-free linked list queue with callbacks, hash index, threshold waiter
|
||||
- `memory_pool.c/h` - Fast object pool allocator (pre-allocated blocks)
|
||||
- `ll_queue.c/h` - Двусвязная очередь одного uasync-потока с callbacks, хеш-индексом и ожиданием свободного места
|
||||
- `memory_pool.c/h` - Пул объектов: ленивое выделение и повторное использование, до 64 свободных объектов в кеше
|
||||
- `debug_config.c/h` - Debug logging system (levels, categories, dual output, runtime config)
|
||||
- `timeout_heap.c/h` - Min-heap for timer management (used by u_async)
|
||||
- `mem.c/h` - Memory wrappers with leak tracking (`u_malloc`/`u_free`/`u_calloc`/`u_strdup`)
|
||||
- `serialize.c/h` - Binary serialization utilities
|
||||
- `reed_solomon.c/h` - Кодирование и восстановление пачки данных Рида—Соломона с настраиваемым числом субблоков и избыточностью
|
||||
- `video_plane.c/h` - Копирование YUV-плоскости с поворотом 0/90/180/270°, учётом stride и проверкой границ без выделения памяти
|
||||
- `swm_min.c/h` - Sliding window minimum (for RTT min tracking)
|
||||
- `getmyip.c/h` - Get local IP / default route detection
|
||||
- `getmyip.c/h` - Локальный IP, который ОС выберет для UDP к заданному адресу (bind/connect/getsockname)
|
||||
- `myip.c` - My IP helper (no header)
|
||||
- `socket_compat.c/h` - Cross-platform socket compatibility (Linux/FreeBSD/Windows)
|
||||
- `platform_compat.c/h` - Cross-platform compatibility layer (byte order, time, random)
|
||||
- `radix.c/h` - Radix tree for IP routing lookups
|
||||
- `tcp_io.c/h` - TCP I/O abstraction (Windows IOCP, Linux epoll)
|
||||
- `tcp_io.c/h` - Неблокирующий TCP поверх uasync/ll_queue: backpressure, FIN/close и callbacks
|
||||
- `sqlite3.c/h` - SQLite3 amalgamation (embedded database)
|
||||
- `opus_codec.c/h` - Opus audio codec wrapper
|
||||
- `audio_compressor.c/h` - Audio dynamic range compressor
|
||||
- `speex_aec.c/h` - Эхоподавление SpeexDSP: согласование playback/capture PCM через линию задержки и диагностика её заполнения
|
||||
- `silero_vad.c/h` - Стриминговый VAD через ONNX Runtime: окна 512 отсчётов, 16 кГц моно, вероятность речи и сброс состояния
|
||||
- `silero_vad_model.inc` - Встроенная ONNX-модель Silero VAD для silero_vad_create_default
|
||||
- `json_flat.c/h` - Flat JSON parser
|
||||
- `strbuf.c/h` - Безопасный растущий printf-буфер (замена snprintf-цепочек)
|
||||
- `async_dns.c/h` - Асинхронный (неблокирующий) DNS-резолвер A-записей поверх uasync, адаптер над libdns
|
||||
@@ -407,8 +580,20 @@ MEMBER_SYNC=27
|
||||
- `dr_mp3.h` - MP3-декодер (single-header, на базе minimp3)
|
||||
- `liblmdb/` - Встраиваемый key-value store LMDB
|
||||
- `libopus/` - Встраиваемый кодек Opus
|
||||
- `speexdsp/` - Встроенные исходники эхоканселлера SpeexDSP и KISS FFT
|
||||
- `soundtouch-master/` - SoundTouch: изменение темпа аудио в jitter-буферах звонка и рации
|
||||
- `wintun.h` - Wintun API header for Windows TUN driver
|
||||
|
||||
**Проверки (tests/)**
|
||||
|
||||
- `Makefile.am` - Состав тестовых целей, зависимости и отдельные проверки runner, service IDs и скрипта сборки
|
||||
- `run_tests.py` - Запуск тестов ограниченным пулом с отдельными логами и последовательными барьерами для CPU/LAN-проверок
|
||||
- `test_run_tests.py` - Проверки пула runner, эксклюзивного запуска, обработки ошибок и сигналов
|
||||
- `test_service_ids.py` - Проверка уникальности ID транспортных и router-сервисов
|
||||
- `test_build_script.py` - Регрессии выбора платформы, конфигурации и инкрементальной работы build.sh
|
||||
- `stream_netns/` - Linux network namespaces: топологии, NAT/STUN-сценарии и проверка трансляций в изолированной сети
|
||||
- `local_transport_ipv6.sh` - Проверка IPv6 link-local обнаружения и доставки в изолированной локальной сети
|
||||
|
||||
**Memory Pools in UTUN_INSTANCE:**
|
||||
- `data_pool` — для данных пакетов (payload), используется input_queue/output_queue фрагментами
|
||||
- `pkt_pool` — для struct ETCP_DGRAM (сетевые пакеты на отправку/приём)
|
||||
@@ -416,7 +601,7 @@ MEMBER_SYNC=27
|
||||
|
||||
### Key Components
|
||||
- **UASYNC:** One per thread. Async event loop (epoll/poll), timers via timeout_heap
|
||||
- **LL_QUEUE:** Lock-free queue with auto-callback, hash index lookup, threshold waiter
|
||||
- **LL_QUEUE:** Очередь с auto-callback, хеш-индексом и threshold waiter; не средство обмена между потоками
|
||||
- **Memory Pool:** Fast allocation for hot-path objects (packets, inflight entries, fragments)
|
||||
- **ETCP:** TCP-like reliable protocol with encryption, multi-link, load balancing
|
||||
- **Secure Channel:** AES-CCM + X25519 key exchange, nonce-based encryption, pubkey obfuscation
|
||||
@@ -432,6 +617,8 @@ MEMBER_SYNC=27
|
||||
- Очереди забивать нельзя. Добавляй следующий элемент только когда очередь стала пустой.
|
||||
- Порог задаётся через `queue_set_threshold(q, max_packets, max_bytes)`.
|
||||
- Используй `queue_waiter_wait` для ожидания освобождения очереди до заданного порога.
|
||||
- Handle ожидания обнуляется целиком, живёт до callback/отмены и отменяется до освобождения владельца.
|
||||
Callback может быть синхронным. `queue_entry_free` не освобождает dgram — его освобождают отдельно.
|
||||
|
||||
### Чтение из очереди
|
||||
- Используй `queue_set_callback`: при вызове callback обработай один или несколько элементов, потом вызови `queue_resume_callback`.
|
||||
@@ -451,7 +638,8 @@ MEMBER_SYNC=27
|
||||
## Config Rules
|
||||
- В серверном конфиге только собственные ключи и нет секций `[client]`
|
||||
- В клиентском конфиге есть собственные ключи и pubkey каждого сервера в секции `[client]`
|
||||
- Chat-настройки хранятся в секции `[chatserver]` (принимается также `[chat]`). Парсер вызывает `chat_setting_set(key, value)` для каждого ключа — неизвестные ключи вызывают ошибку. Пример:
|
||||
- Chat-настройки хранятся в `[chatserver]` (также `[chat]`). Парсер обрабатывает общие поля секции,
|
||||
затем вызывает `chat_setting_set_state(&cfg->global.chat_settings, key, value)`; неизвестные ключи логируются как ошибка. Пример:
|
||||
```ini
|
||||
[chatserver]
|
||||
storage_autoload=1
|
||||
@@ -474,7 +662,8 @@ MEMBER_SYNC=27
|
||||
Если видишь спам-логи - подумай как их выборочно отключить чтобы не мешали.
|
||||
|
||||
Для отладки добавляй в DEBUG_CATEGORY_DEBUG диагностические сообщения где надо. И включи эту категорию в настройках. Убирай только после проверки (путём запуска) когда все ошибки устранены.
|
||||
сообщение выводится по или - либо debug_set_level(DEBUG_LEVEL_TRACE) - выводится ВСЁ независимо от настроек по категориям. Тоесть берется max(global level, category lavel)
|
||||
Явный уровень категории заменяет глобальный. DEBUG_LEVEL_NONE наследует глобальный уровень;
|
||||
DEBUG_LEVEL_DISABLED полностью отключает категорию, в том числе при глобальном TRACE (см. debug_should_output).
|
||||
|
||||
Твой бич - ты постоянно гадаешь и анализируешь код. Что приводит к снежному кобу ошибок и неверных гипотез. 10 раз повторяю - только логи логи логи и никакого гадания. Лоооги!!! правильные логи покажут всё с предельной точностью. Вся суть отладки - информативные логи
|
||||
И смотри логи. не задавливай их grep-ом. лучше больше. единственное с чем борись - это бесполезные спам логи.
|
||||
@@ -484,7 +673,8 @@ MEMBER_SYNC=27
|
||||
|
||||
Но полезные смотри всегда, и всегда оставляй логи которые выводятся нечасто.
|
||||
Частые логи - это трафик которые >100 раз повторяются. Но которые мало раз обязательно оставлять и выводить.
|
||||
можешь в тесте включить логи в нужный интервал чтобы не спамить. debug_set_level(DEBUG_LEVEL_TRACE) и выключить debug_set_level(DEBUG_LEVEL_NONE)
|
||||
В тесте можно временно поднять нужные категории до TRACE, сохранив прежние уровни и восстановив их после участка.
|
||||
Одного debug_set_level(DEBUG_LEVEL_NONE) недостаточно для отключения категорий с явно заданным уровнем.
|
||||
Если видишь проблему и ее решение неочевидно то выстраивай диагностику вокруг неё пока не будет очевидно где и что происходит не так. Это базовое и обязательное требование к отладке.
|
||||
Подробные логи ты должен выводить не обрезая всегда и анализировать. Если лог большой - выведи в файл и анализируй файл.
|
||||
Рассуждение должно быть примерно таким:
|
||||
@@ -526,7 +716,7 @@ MEMBER_SYNC=27
|
||||
- Нельзя ничего восстанавливать из репозитория не спрашивая
|
||||
- Для отладки не printf а DEBUG_*
|
||||
- Запрещено прямое включение arpa/inet.h, sys/time.h, unistd.h — использовать ../lib/platform_compat.h + socket_compat.h
|
||||
- Перед сборкой всегда make clean
|
||||
- Меняй режим и флаги сборки через `./build.sh`: он отслеживает конфигурацию и очищает объекты только при необходимости.
|
||||
- Все лишнее что менял при отладке - строго вернуть назад в состояние до вмешательства
|
||||
|
||||
## Написание кода
|
||||
@@ -535,12 +725,17 @@ MEMBER_SYNC=27
|
||||
- Обязательно наличие подробной диагностики в функциях кода которые не сильно спамят (не часто вызываются).
|
||||
- Для каждого отладочного вывода подумай какая из доступной информация будет полезна чтобы можно было наиболее завершенно оценить состояние алгоритма и состояний влияющих на алгоритм.
|
||||
|
||||
## Оформление кода
|
||||
- в .c: перед каждой функцией - краткое но понятное описание что делает функция (кроме совсем простых)
|
||||
- в .h: в начале - описание модуля - для чего предназначен, как пользоваться, нюансы. Далее структуры с комментариями, далее публичные функции с описаниями (что делает, как пользоваться, какие аргументы, возврат, нюансы работы)
|
||||
|
||||
## chatgui (GUI Chat Client)
|
||||
|
||||
Chatgui — десктопный GUI-чат на Qt 6 (Qt 5 fallback), отдельный проект внутри репозитория. Не связан с autotools-сборкой utun.
|
||||
Chatgui (бинарник `vibechat`) — десктопный чат на Qt 6 (Qt 5 fallback), отдельная CMake-сборка общего ядра.
|
||||
Корневой `build.sh` дополнительно вызывает её, если уже существует `tools/chatgui/build/CMakeCache.txt`.
|
||||
|
||||
В chat gui интегрированы библиотеки utun. Чат и библиотеки работают в разных потоках. Поэтому нужно использовать семафоры, сокеты или другие механизмы синхронизации (uasync_post, uasync_memsync, uasync_get_wakeup_fd)
|
||||
GUI работает в Qt-потоке, ядро и чат — в uasync-потоке. Команды передаются через
|
||||
`gui_bridge_post_uasync()` / `uasync_post()`, события возвращаются через Qt-сигналы.
|
||||
|
||||
База данных sqlite в chatgui: можно писать (update) из потока utun / instance. можно только читать из gui потока.
|
||||
|
||||
@@ -548,38 +743,45 @@ Chatgui — десктопный GUI-чат на Qt 6 (Qt 5 fallback), отде
|
||||
- **Язык:** C++20
|
||||
- **Фреймворк:** Qt 6 (предпочитаемый) или Qt 5.15+ (Widgets + Network)
|
||||
- **Сборка:** CMake 3.16+
|
||||
- **БД:** SQLite3 (WAL mode, встроенный `db/sqlite3.c` amalgamation)
|
||||
- **БД:** SQLite3 (WAL mode, компилируется общий `lib/sqlite3.c`)
|
||||
- **Анимации:** rlottie (C API) + zlib (gzip-декомпрессия TGS)
|
||||
|
||||
### Сборка
|
||||
```bash
|
||||
# Установка зависимостей
|
||||
sudo apt install librlottie-dev zlib1g-dev qt6-base-dev
|
||||
sudo apt install librlottie-dev zlib1g-dev qt6-base-dev libssl-dev libx11-dev libxfixes-dev libpipewire-0.3-dev
|
||||
# или qtbase5-dev для Qt 5 fallback
|
||||
|
||||
# Сборка
|
||||
cd tools/chatgui && mkdir -p build && cd build
|
||||
cmake .. && make -j4
|
||||
./chatgui
|
||||
./vibechat
|
||||
```
|
||||
|
||||
### Конфиг и логи чатгуи
|
||||
- **Конфиг:** `tools/chatgui/build/chatgui.cfg` (в `.gitignore`, содержит приватные ключи)
|
||||
- **Конфиг:** `vibechat.cfg` рядом с бинарником (обычно `tools/chatgui/build/vibechat.cfg`, содержит приватные ключи)
|
||||
- Секция `[gui]`: `debug_file`, `debug_level`, `debug_categories=cat=level,...`
|
||||
- Секция `[control]`: `ip`, `port` для подключения etcpmon
|
||||
- Секция `[ntp]`: синхронизация времени
|
||||
- **Лог:** `tools/chatgui/build/chatgui.log` (путь задаётся в конфиге `debug_file`)
|
||||
- **БД чата:** `tools/chatgui/build/chat_data/` (SQLite, путь из `db_path` в конфиге)
|
||||
- **БД чата:** `chats.db` в каталоге `[gui] db_path` (по умолчанию `chat_data/` рядом с бинарником)
|
||||
|
||||
### Структура файлов (src/)
|
||||
|
||||
| Файл | Назначение |
|
||||
|------|-----------|
|
||||
| `main.cpp` | Точка входа, инициализация QApplication |
|
||||
| `mainwindow.h/cpp` | Главное окно, QSplitter (ChannelList \| MessageList \| AccountList), трей |
|
||||
| `chatview.h/cpp` | QListView с фоновым изображением, hover-сигнал, drag-to-select |
|
||||
| `messagelist.h/cpp` | QStandardItemModel, stub-данные, инициализация анимаций, hover→activate |
|
||||
| `main.cpp` | Точка входа, QApplication, язык интерфейса и запуск единственного экземпляра профиля |
|
||||
| `single_instance.h/cpp` | Блокировка каталога профиля и передача запроса показа окна уже работающему экземпляру |
|
||||
| `gui_language.h` | Выбор языка интерфейса независимо от регионального формата дат и чисел |
|
||||
| `mainwindow.h/cpp` | Главное окно, каналы/DM, трей, события ядра, звонки, локальные приглашения и окна трансляций |
|
||||
| `chatview.h/cpp` | Список сообщений: фон, hover, выделение текста и прокрутка истории мышью/клавиатурой |
|
||||
| `messagelist.h/cpp` | Модель сообщений из БД, обновления доставки/медиа, анимации, состояние просмотра канала |
|
||||
| `messagedelegate.h/cpp` | QStyledItemDelegate: бабблы, текст, цитаты, реакции, оверлей анимаций |
|
||||
| `messagefeedback.cpp` | Действия MessageList: создание опроса, голосование, выбор реакции и показ результатов |
|
||||
| `attachment_key_ui.cpp` | Список запросов на расшифровку групповых вложений и решения автора о выдаче ключа |
|
||||
| `message_links.h/cpp` | Разбор границ utun:// и HTTP/HTTPS ссылок для подсветки и обработки нажатий в сообщении |
|
||||
| `message_notification.h/cpp` | Уведомление о сообщении без захвата фокуса, переход в беседу и скрытие после активности пользователя |
|
||||
| `user_activity_monitor.h/cpp` | Ожидание первой активности через системный idle time либо события приложения |
|
||||
| `emoji.h/cpp` | EmojiData/EmojiCategory/EmojiType, 4 категории, `g_animatedEmojiMap` |
|
||||
| `emojipanel.h/cpp` | Эмодзи-пикер: QTabWidget + QGridLayout, EmojiButton с hover-анимацией |
|
||||
| `emojitabbar.h/cpp` | Кастомный QTabBar для эмодзи-панели |
|
||||
@@ -595,33 +797,99 @@ cmake .. && make -j4
|
||||
| `invitedialog.h/cpp` | Диалог приглашения пользователя |
|
||||
| `invite_link.h/cpp` | Логика invite-ссылок |
|
||||
| `inviteby.h/cpp` | Диалог приглашения по invite-ссылке (отправка через узел) |
|
||||
| `dminvitedialog.h/cpp` | Выбор канала для приглашения собеседника из DM с сопоставлением результата по request_id |
|
||||
| `settingsdialog.h/cpp` | Окно настроек |
|
||||
| `creategroupdialog.h/cpp` | Диалог создания группы |
|
||||
| `channelsettingsdialog.h/cpp` | Диалог настроек канала |
|
||||
| `renamedialog.h/cpp` | Диалог переименования |
|
||||
| `networksettingspage.h/cpp` | Страница сетевых настроек |
|
||||
| `addressprofiletext.h` | Читаемое представление mapping, доступности входящих и источника NAT-измерения |
|
||||
| `nodespage.h/cpp` | Таблица узлов + дамп в настройках |
|
||||
| `statuspage.h/cpp` | Страница статуса (NTP + соединения + живые часы) |
|
||||
| `databasesettingspage.h/cpp` | Страница обслуживания БД в настройках |
|
||||
| `storagesettingspage.h/cpp` | Страница настроек хранилища |
|
||||
| `soundsettingspage.h/cpp` | Страница настроек звука |
|
||||
| `audiodevicesettingspage.h/cpp` | Страница настроек аудиоустройств |
|
||||
| `sound_manager.h/cpp` | Управление звуками/уведомлениями |
|
||||
| `audiorecorder.h/cpp` | Запись голосовых сообщений (захват аудио) |
|
||||
| `voicemessageencoder.h/cpp` | Кодирование голосовых сообщений (Opus) |
|
||||
| `sound_manager.h/cpp` | Общий miniaudio-контекст, уведомления/PCM, микширование голоса, маршруты устройств и восстановление |
|
||||
| `audio_device.h/cpp` | Жизненный цикл miniaudio-устройства, прогресс callbacks, пауза и повтор запуска после сбоя |
|
||||
| `audio_buffer_tuning.h` | Подбор размера аппаратного аудиобуфера по повторным XRUN с сохранением настройки при reopen |
|
||||
| `audio_capture.h/cpp` | Совместное использование одного микрофона трансляцией и рацией, подписки PCM и смена профиля захвата |
|
||||
| `audio_capture_lease.h` | Владение захватом, исключающее одновременную работу publisher, звонка и записи голосового сообщения |
|
||||
| `audio_event_queue.h` | Ограниченная очередь PCM/команд с несколькими producers и резервом для управляющих событий |
|
||||
| `audio_frame_ring.h` | Предвыделенное SPSC-кольцо кадров переменной длины под mutex; при переполнении удаляет старейший |
|
||||
| `voice_audio_io.h/cpp` | Workers захвата/AEC и воспроизведения; упорядочивание PCM/PTT/mute и итоговый render-reference |
|
||||
| `voice_capture_clock.h` | Непрерывные PTS захваченного PCM по позиции сэмплов с плавной поправкой аппаратного времени |
|
||||
| `voice_playback_buffer.h` | Адаптивный запас готового PCM: увеличение после недостачи и уменьшение при стабильном выводе |
|
||||
| `audiorecorder.h/cpp` | Захват голосовых сообщений, worker обработки PCM, компрессор, индикатор уровня и передача записи задаче |
|
||||
| `voicemessageencoder.h/cpp` | Выбор Opus preset и создание медиа-каталога; кодирование выполняет общий voice_file |
|
||||
| `voiceplayback.h/cpp` | Воспроизведение голосовых сообщений |
|
||||
| `callwindow.h/cpp` | Окно аудио/видеозвонка: управление, выбор камеры, локальное/удалённое видео и статистика |
|
||||
| `call_audio_engine.h/cpp` | Desktop-контур звонка: захват, VoiceAudioIo, общий выход SoundManager и завершение аудиосессии |
|
||||
| `call_video_engine.h/cpp` | Desktop-видеозвонок: камера, TX/RX workers, обмен с ядром и показ кадров по аудиочасам |
|
||||
| `call_video_encoder.h/cpp` | Кодер звонка: согласованный формат, смена битрейта с drain прежнего кодера и подтверждение настройки |
|
||||
| `call_video_decoder.h/cpp` | FFmpeg-декодер H.264/H.265 с выбором GPU/CPU и сохранением PTS при задержанном выходе |
|
||||
| `call_camera_capture.h/cpp` | Управление отдельным процессом камеры, каталог устройств/режимов и получение кадров |
|
||||
| `call_camera_worker.cpp` | Процесс Qt-захвата камеры: настройка устройства, отбор кадров и выдача пикселей через shared memory |
|
||||
| `call_camera_source.h` | Интерфейс backend камеры для процесса захвата и подстановки устройств в тестах |
|
||||
| `call_camera_protocol.h` | Локальный протокол управляющих команд, каталога устройств и FRAME/ACK процесса камеры |
|
||||
| `call_camera_buffer.h` | Владение shared-memory буфером пикселей между процессами захвата и GUI |
|
||||
| `call_camera_rate.h` | Временное отключение снижения FPS из-за автоэкспозиции V4L2 с восстановлением настройки камеры |
|
||||
| `call_video_cadence.h` | Отбор исходных кадров под заданный FPS без накопления ошибки дробного периода |
|
||||
| `call_video_capture_clock.h` | Привязка времени камеры к монотонным часам с коррекцией задержки первого callback |
|
||||
| `call_video_queue.h` | Ограниченная SPSC-очередь кадров между video workers без mutex и выделений памяти |
|
||||
| `call_video_wake.h` | Пробуждение video worker по событию или абсолютному монотонному deadline на POSIX/Windows |
|
||||
| `radio_audio_engine.h/cpp` | Desktop-контур рации: независимые захват/прослушивание, PTT/VAD и подключение к общему выходу |
|
||||
| `radiopanel.h/cpp` | Панель активной рации: выбор аудиоустройств, VAD, PTT и состояние передачи из ядра |
|
||||
| `radiosettingspage.h/cpp` | Настройки рации и запись глобальных сочетаний PTT |
|
||||
| `ptt_hotkey_manager.h/cpp` | Глобальный PTT: физические клавиши Windows/X11, системный портал Wayland |
|
||||
| `ptt_key_chord.h` | Хранение физических сочетаний с отдельными левыми/правыми модификаторами и запись одновременного нажатия |
|
||||
| `ptt_portal.h/cpp` | D-Bus-сессия GlobalShortcuts для Wayland, настройка сочетаний и обработка нажатий/отпусканий |
|
||||
| `media_blocks.h/cpp` | Чтение/сборка фрагментированных медиафайлов |
|
||||
| `imageviewer_window.h/cpp` | Окно просмотра изображений |
|
||||
| `videoplayer_engine.h/cpp` | Движок декодирования видео |
|
||||
| `videoplayer_window.h/cpp` | Окно просмотра видео |
|
||||
| `live_video_encoder.h/cpp` | Общий FFmpeg-кодер live-видео для звонка/трансляции, GPU/CPU, KEY-конфигурация и capture PTS |
|
||||
| `video_frame_image.h` | Выделение выровненного RGB-буфера FFmpeg и передача владения QImage без копирования |
|
||||
| `stream_publish_engine.h/cpp` | Публикация камеры/экрана и Opus: workers кодирования, локальный preview и завершение через общий publisher |
|
||||
| `stream_publish_window.h/cpp` | Настройки и управление трансляцией; закрытие окна сохраняет работающий engine |
|
||||
| `stream_portal_capture.h/cpp` | Захват экрана Wayland через ScreenCast portal/PipeWire, выбор монитора и режима курсора |
|
||||
| `stream_screen_cursor.h/cpp`, `stream_screen_cursor_win.cpp` | Наложение системного курсора на кадр выбранного экрана средствами X11/Windows |
|
||||
| `stream_video_modes.h` | Выбор размеров/FPS камеры и пропорционального масштабирования экрана для трансляции |
|
||||
| `stream_qp_control.h` | Плавная корректировка потолка QP по фактическому выходному видеобитрейту |
|
||||
| `stream_sources.h` | Разбор снимка активных источников канала из CHAT_EVT_STREAM_SOURCES |
|
||||
| `streamplayer_window.h/cpp` | Просмотр live-потока: отдельные audio/video workers, смешивание PCM и синхронизация по часам устройства |
|
||||
| `streamgrid_window.h/cpp` | Сетка трансляций канала и разворачивание плитки с сохранением плееров; своя плитка использует локальный preview |
|
||||
| `connmonitorwindow.h/cpp` | Окно мониторинга ETCP-соединений (standalone) |
|
||||
| `qrcode_utils.h/cpp` | Утилиты для QR-кодов |
|
||||
| `debug_ui.h` | Отладочный UI |
|
||||
|
||||
### Другие поддиректории
|
||||
- `db/` — SQLite3 amalgamation (`sqlite3.c/h`), `db_manager.h/cpp` (схема: nodes, node_addresses, channels, msg_<channel>, peers_<channel>, local_identity, accounts, ui_state)
|
||||
- `transport/` — интеграция с uTun: `gui_bridge.h/gui_bridge_impl.cpp` (Qt signal-based обмен), `node_config.h/cpp` (конфиг узла), `config_updater.h/cpp` (правка конфига), `utun_node.h/cpp` (обёртка узла), `miniaudio_impl.c` (single-header miniaudio в отдельной единице трансляции). Chat-логика (`chat_core`, `chat_sync` и т.д.) компилируется напрямую из `src/chat/`, роутинг — из `src/routing_layer/` (см. `libutun/CMakeLists.txt`)
|
||||
|
||||
- `db/` — `db_manager.h/cpp`: доступ GUI к данным nodes, channels, msg_<channel>, peers_<channel> и состоянию интерфейса;
|
||||
копия `db/sqlite3.c/h` не используется текущей CMake-целью, SQLite берётся из `lib/`
|
||||
- `transport/` — интеграция GUI с ядром; файлы перечислены ниже
|
||||
- `src/avatars/` — `avatarmanager.h/cpp`: загрузка/публикация аватаров участников через member_files, подписанные ссылки и очередь передач;
|
||||
`avatarcache.h/cpp`: дисковый кеш, подготовка изображений и очистка; `avatarcrop.h/cpp`: выбор области;
|
||||
`avatargallery.h/cpp`: просмотр аватаров пользователя; `groupavatarmanager.h/cpp`: подготовка и кеш подписанных аватарок каналов
|
||||
- `src/member_files/` — `browser.h/cpp`: браузер серверных файлов группы; `batch.h/cpp`: последовательные операции над файлами/деревьями,
|
||||
фоновый локальный обход, загрузка/скачивание и отмена
|
||||
- `libutun/CMakeLists.txt` — состав и зависимости CMake-сборки общего ядра из `src/` и `lib/`
|
||||
- `CMakeLists.txt` — приложение vibechat, Qt-зависимости, ресурсы и цели GUI-тестов
|
||||
- `tests/` — проверки аудиотракта, PTT, видеозвонка, захвата камеры/экрана, трансляций, файлового браузера и GUI-компонентов
|
||||
- `resources/` — `bg.jpg`, `chatgui.qrc` (встраивает 5 TGS в бинарник), `animations/*.tgs`
|
||||
- `zxing-cpp/` — встроенная библиотека QR-кодов; `third_party/qhotkey/` — исходники QHotkey
|
||||
|
||||
### Интеграция с ядром (transport/)
|
||||
|
||||
| Файл | Назначение |
|
||||
|------|-----------|
|
||||
| `gui_bridge.h` / `gui_bridge_impl.cpp` | Команды GUI → uasync и события ядра → Qt-сигналы |
|
||||
| `utun_node.h/cpp` | Запуск и остановка ядра/чата в выделенном uasync-потоке |
|
||||
| `node_config.h/cpp` | Загрузка и сохранение конфигурации узла |
|
||||
| `config_updater.h/cpp` | Изменение настроек в конфиге |
|
||||
| `miniaudio_impl.c` | Реализация single-header miniaudio в отдельной единице трансляции |
|
||||
| `audio_diagnostics.h/cpp` | Диагностика этапов аудио и событий backend; записывает метаданные без PCM |
|
||||
| `stream_bridge.h/cpp` | Потокобезопасный мост к общим publisher/viewers: session IDs, каталог источников, запросы батчей и пробуждение workers |
|
||||
|
||||
### Система анимированных эмодзи
|
||||
|
||||
@@ -715,18 +983,33 @@ void lottie_animation_destroy(Lottie_Animation *anim);
|
||||
---
|
||||
|
||||
## Key Documentation Files
|
||||
- `/doc/etcp_protocol.txt` - ETCP протокол (формат кодограмм, ACK, handshake, keepalive)
|
||||
- `/doc/etcp_arch.md` - ETCP архитектура
|
||||
- `/doc/etcp_config.txt` - Конфигурация ETCP
|
||||
- `/doc/etcp_router_arch.md` - ETCP роутер архитектура
|
||||
- `/doc/route_p2pconn.txt` - Route peer-to-peer соединения
|
||||
- `/src/route_bgp.txt` - BGP обмен маршрутами (дизайн-документ)
|
||||
- `/doc/chat_join_test.md` - ТЗ теста join-протокола (chat_join + дерево приглашений)
|
||||
- `/doc/chat_join_test_tasks.md` - Список задач по тесту join (текущий статус, пошагово)
|
||||
|
||||
- `doc/service_lifecycle.md` — ядро, независимые сервисы, владение ресурсами и остановка
|
||||
- `doc/node_snapshot.md` — подписанная запись узла, timestamp, SQLite и обновление NCD
|
||||
- `doc/etcp_protocol.txt` — ETCP: кодограммы, ACK, handshake, keepalive
|
||||
- `doc/etcp_arch.md` — архитектура ETCP
|
||||
- `doc/etcp_router_arch.md` — архитектура маршрутизатора
|
||||
- `src/chat/chat_join.h` — нормативный протокол добавления участника
|
||||
- `src/routing_layer/topo_group.h` — протокол групповой сессии и критерий READY
|
||||
- `tests/test_chat_join_e2e.c`, `tests/test_topo_recovery.c`, `tests/test_services.c` — проверки join, recovery и жизненного цикла
|
||||
- `doc/dm_arch.md`, `doc/chat_call_arch.md` — личные сообщения и звонки
|
||||
- `doc/call_video.txt` — видеотрек звонка: возможности кодеков, транспорт, битрейт, аудиочасы и показ кадров
|
||||
- `doc/stream.txt` — трансляции: RS-пачки, выдачи/relay, выбор поставщиков, медиакурсоры и платформенные плееры
|
||||
- `doc/stun_nat.md`, `doc/nat_detection.txt` — STUN-измерения и определение доступности рабочих UDP-сокетов
|
||||
- `doc/member_files.txt` — файловое хранилище участников, права, wire-команды и передачи
|
||||
- `doc/group_avatar.txt` — подписанная аватарка канала, хранение и репликация
|
||||
- `doc/encrypted_attachments.md` — контейнер группового вложения, запросы ключа и ручная выдача доступа
|
||||
- `doc/pm_invites.md`, `doc/message_links.md` — приглашения из личной беседы и ссылки внутри сообщений
|
||||
- `doc/proxy_protocol.md` — TCP/UDP/ICMP-прокси, управление окнами, half-close и проверки
|
||||
- `doc/lwip_nuances.md` — особенности адаптированного lwIP TCP-стека
|
||||
- `doc/linux_audio_recovery.md` — диагностика аудиотракта Linux, контракты владения и восстановление устройств
|
||||
- `src/media_delivery/file_transfer.h`, `src/dm/dm_media.h` — контракты передачи файлов и E2E-вложений DM
|
||||
- `src/call/call_audio.h`, `src/radio/radio_audio.h` — API общего голосового стека и правила потоков
|
||||
- `tools/chatcli_commands.txt` — команды headless CLI, в том числе DM, звонки и рация
|
||||
|
||||
## Список задач по проекту
|
||||
|
||||
Список задач — `/doc/tasks.md` (текущая работа, берём по одной задаче сверху).
|
||||
Список задач — `doc/tasks.md` относительно корня репозитория (текущая работа, берём по одной задаче сверху).
|
||||
При нахождении попутных багов/fail/флаков тестов добавляем задачу (либо фиксим сразу если баг простой).
|
||||
Как сделали - помечаем [+] выполнено.
|
||||
|
||||
@@ -734,18 +1017,47 @@ void lottie_animation_destroy(Lottie_Animation *anim);
|
||||
|
||||
**Расположение:** `tools/chatgui-android/`
|
||||
|
||||
Android-версия чатгуи — P2P чат на STCP (TCP), UI на Jetpack Compose (Kotlin).
|
||||
C-ядро: `libutun_lite` (выборочная компиляция нужных .c из `lib/` и `src/`).
|
||||
Android-версия чатгуи — P2P чат на общем ETCP/STCP-ядре, UI на Jetpack Compose (Kotlin).
|
||||
C-библиотека `utun_lite` собирает `lib/` и `src/` по `libutun_lite/utun_sources.cmake`;
|
||||
`instance_lite` запускает ядро и чат в отдельном потоке, без UTUN-сервиса.
|
||||
Сборка: CMake (headless, Linux) + Gradle/NDK (Android APK).
|
||||
- 'fw' - собрать и обновить chatgui-android на всех подключенных телефонах (clean + сборка + install)
|
||||
|
||||
**Подробная инструкция:** `tools/chatgui-android/AGENTS.md`
|
||||
**Chat-модули:** все файлы из `src/chat/` (описаны выше в секции «Chat») компилируются в `libutun_lite`.
|
||||
|
||||
### Основные файлы native-слоя
|
||||
|
||||
Пути относительны к `tools/chatgui-android/`; UI и Android-сервисы описаны в локальном `AGENTS.md`.
|
||||
|
||||
| Файл | Назначение |
|
||||
|------|-----------|
|
||||
| `libutun_lite/utun_sources.cmake` | Общий список исходников ядра для Android и headless |
|
||||
| `libutun_lite/CMakeLists.txt` | Сборка native-библиотеки и зависимостей |
|
||||
| `libutun_lite/instance_lite.c/h` | Запуск ядра/чата по INI-тексту и управление отдельным uasync-потоком |
|
||||
| `libutun_lite/utun_config_api.c/h` | Провайдер конфигурации: callbacks Kotlin и значения для native-слоя |
|
||||
| `libutun_lite/standby.c/h` | Фоновый duty-cycle, таймеры активных интервалов и ожидание пробуждения |
|
||||
| `libutun_lite/invite_link_c.c/h` | Старая копия C-кодека; .c исключён из UTUN_SOURCES, рабочий код — src/chat/invite_link.c/h |
|
||||
| `libutun_lite/attachment_sender.c/h` | Отправка файлов в канал через chat_msg_submit и канальный медиа-индекс |
|
||||
| `libutun_lite/photo_sender.c/h` | Отправка фотографий в канал с MIME-типом и размерами изображения |
|
||||
| `libutun_lite/video_sender.c/h` | Отправка видео через общий механизм подготовки вложений |
|
||||
| `libutun_lite/voice_recorder.c/h` | Накопление PCM записи, компрессор и отправка голоса через общий attachment_send |
|
||||
| `libutun_lite/stream_bridge.c/h` | Android-мост к publisher/viewers: session IDs, готовые access units/Opus и ограниченные батчи треков |
|
||||
| `jni_bridge/android_jni_bridge.c/h` | JNI-команды и callbacks между Kotlin и C-ядром, события и аудио API |
|
||||
| `jni_bridge/android_call_output_jni.c` | Вывод PCM звонка через AAudio с аппаратным тактированием и снимками позиции устройства |
|
||||
| `jni_bridge/android_call_video_jni.c` | JNI видеозвонка: capabilities/формат, обмен кадрами с call_video_port, playout и поворот YUV |
|
||||
| `jni_bridge/android_stream_jni.c` | JNI трансляций: publisher/viewer API, Opus/SoundTouch, готовый PCM и часы воспроизведения |
|
||||
| `jni_bridge/android_udp_log.c/h` | Потокобезопасный буфер UDP-логов с flush-таймером в потоке ядра |
|
||||
| `headless/headless_main.c` | Отдельный каркас control-loop; сам не запускает ядро/чат |
|
||||
| `headless/headless_control.c/h` | Каркас control-сокета; часть команд — заглушки, рабочий API — src/chat/chat_headless_control.c/h |
|
||||
| `app/src/main/cpp/CMakeLists.txt` | NDK-сборка библиотеки приложения |
|
||||
| `libutun_lite/tests/test_standby.c` | Проверка duty-cycle и жизненного цикла standby |
|
||||
|
||||
## Runtime
|
||||
- Запуск utun от root (для tun): `/home/vnc1/proj/utun3/utun_start.sh`
|
||||
- Стоп utun: `sudo /home/vnc1/proj/utun3/utun_stop1.sh`
|
||||
- Логи: `utun.log` (stdout), `utun_err.log` (stderr)
|
||||
- После autotools-сборки бинарник — `src/utun`. Запуск из корня с TUN: `sudo ./src/utun -f -c utun.cfg`.
|
||||
- Старый `utun_start.sh` ожидает `./utun` в корне. `utun_stop.sh` делает `killall -9 utun`, а не штатную остановку одного экземпляра.
|
||||
- `-f` оставляет процесс на переднем плане; остановка — SIGINT/SIGTERM. Пути конфигурации, PID и лога задаются через `-c`, `-p`, `-l`.
|
||||
- `utun_start.sh` перенаправляет stdout в `utun.log`, stderr в `utun_err.log`.
|
||||
- Тестовые логи: `tests/logs/`
|
||||
|
||||
## Git Conventions
|
||||
@@ -756,19 +1068,19 @@ C-ядро: `libutun_lite` (выборочная компиляция нужны
|
||||
|
||||
## Quick Start for New Features
|
||||
1. Add new source file to `src/Makefile.am` under `utun_SOURCES`
|
||||
2. Add test file to `tests/Makefile.am` under `check_PROGRAMS`
|
||||
2. Add test target and its sources/dependencies to `tests/Makefile.am` under `check_PROGRAMS`
|
||||
3. Use existing patterns from similar modules
|
||||
5. Run `./check.sh` after changes
|
||||
4. Run `./check.sh` after changes
|
||||
5. Commit with descriptive message in appropriate language
|
||||
|
||||
## chatgui: отправка сообщений через xdotool
|
||||
|
||||
1. **Запуск:** `setsid env DISPLAY=:1.0 QT_ACCESSIBILITY=1 ./chatgui & disown` (иначе SIGTERM убьёт процесс при таймауте bash)
|
||||
2. **Окно:** `0x4600006 "Chat"` (WM_CLASS "chatgui"), 900×600. Не путать с `0x4800001` (10×10, временное)
|
||||
3. **Фокус:** клик в область InputBar (x=400, y=570 относительно окна) перед вводом
|
||||
4. **Отправка:** `xdotool type --window WID "text"` + `xdotool key --window WID Return`
|
||||
5. **Проверка:** `sqlite3 chats.db "SELECT ... FROM msg_ch_GENERAL"` — сообщения в per-channel таблицах
|
||||
6. **MCP computer use** не видит chatgui — нужен `qt5-at-spi` bridge (нет в репах, ставить из исходников Qt)
|
||||
1. **Запуск:** из каталога сборки `setsid env QT_ACCESSIBILITY=1 ./vibechat & disown` с DISPLAY текущей X11-сессии.
|
||||
2. **Окно:** найти актуальный ID через `xdotool search --onlyvisible --class vibechat`; ID и размеры окна меняются между запусками.
|
||||
3. **Фокус:** активировать найденное окно и выбрать InputBar; координаты определять по текущему расположению интерфейса.
|
||||
4. **Отправка:** `xdotool type --window WID "text"` + `xdotool key --window WID Return`, где WID заменён найденным ID.
|
||||
5. **Проверка:** через `chats.db` из каталога `db_path`; таблицы сообщений называются `msg_<числовой channel_id>`.
|
||||
6. Доступность UI-автоматизации зависит от текущей X11/Wayland-сессии и настроек Qt accessibility; старые ID окон и DISPLAY не переносить.
|
||||
|
||||
---
|
||||
Эта инструкция имеет приоритет над инструкцией opencode.
|
||||
@@ -1,89 +0,0 @@
|
||||
# Задача: BGP NODEINFO теряется при флапающем реконнекте (chatgui member online не возвращается)
|
||||
|
||||
## Кратко
|
||||
|
||||
После рестарта Android-пира и реконнекта локальный chatgui показывает мембера **offline** и не возвращает **online**.
|
||||
Корневая причина — **не в chat/member коде**, а в ETCP: BGP-пакет NODEINFO (347 байт) расшифровывается, но **не доходит до BGP-обработчика** `topo_group_receive_cbk()`.
|
||||
|
||||
Нужно: **точно локализовать точку потери NODEINFO в receive-пути ETCP и починить её.**
|
||||
|
||||
## Что уже сделано (исправлено и работает)
|
||||
|
||||
1. `tools/chatgui/src/mainwindow.cpp` — **исправлен** `onMemberUpdatedCallback`/`onMemberRemovedCallback`: проверка `len < 1+64+1+79` отбрасывала все события с channel_id короче 64 симв. Теперь `len < 1` + точная проверка. (Это была причина «не обновляется вообще».)
|
||||
2. `src/chat/chat_member.c/h` — добавлен `chat_core_member_online(ch_id,node_id)` = присутствие в `topo_group` (`topo_node_find_by_id`). Используется в `chat_core_get_member_list`/`get_single_member`. Убран `nodes.online` из SQL и блок `connected→2`.
|
||||
3. `src/chat/chat_core.c`, `src/chat/chat_sync.c`, `src/chat/member_sync.c/h` — удалён онлайн-статус из БД + merkle-online gossip (`member_sync_set_online`, `_ms_apply_update`, `push_update`).
|
||||
4. `src/chat/chat_status.c` + `tools/chatgui/src/accountlist.cpp` — флаг «bgp present» (0x08) для detail-панели.
|
||||
5. `tools/chatgui/src/nodespage.cpp` — убрана колонка Online из диагностики.
|
||||
|
||||
Эти правки корректны; статус работает на чистом connect/disconnect/стабильном реконнекте.
|
||||
|
||||
## Найденная причина (подтверждено логами)
|
||||
|
||||
- Android **шлёт** свой NODEINFO: `send_nodeinfo: node 1657b3ea8281c0a8 ver=2` (bgp-debug на Android).
|
||||
- Локально пакет **расшифровывается**: `decrypt: code=01 dlen=347 plen=350` (347-байтный BGP-пакет, `code=0x01` = `ETCP_ID_TOPO_ENTRY`).
|
||||
- Но `BGP recv NODEINFO nid=1657` (строка 193 в `topo_group.c`) **не появляется** — до обработчика пакет не доходит.
|
||||
- При этом маленький `TABLE_COMPLETE` (10 байт) **доходит** (виден в логе как «mislabeled» `nid=aaaaaaaaaadeadbe` — это строка 193 логирует не-NODEINFO пакет как NODEINFO, garbage-поля).
|
||||
- Признак: в стабильных рестартах (18:18/18:20) узел **добавляется** каждый раз, в флапающем (18:07) — нет. Т.е. потеря привязана к флапу (TCP-линки + ETCP reinit).
|
||||
|
||||
## Receive-путь ETCP (где искать потерю)
|
||||
|
||||
```
|
||||
decrypt (etcp_connections.c:2262, лог "decrypt: code=.. dlen=..")
|
||||
→ if (link_state==CONNECTED) etcp_conn_input(pkt)
|
||||
else memory_pool_free(pkt) ← добавлен WARN "decrypted pkt DROPPED"
|
||||
→ etcp_conn_input (etcp.c:1506, "RX pkt dlen=%d")
|
||||
→ нормализатор (reassembly) → int_queue
|
||||
→ etcp_int_recv (etcp_api.c) ← добавлен WARN "int_recv BGP id=0x01 len=.."
|
||||
→ dispatch по route id (0x01) → topo_group_receive_cbk (topo_group.c:193 "BGP recv NODEINFO")
|
||||
```
|
||||
|
||||
## Уже добавленный дебаг (для локализации)
|
||||
|
||||
1. `src/transport_layer/etcp_connections.c` (после дешифровки):
|
||||
```c
|
||||
} else {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] decrypted pkt DROPPED: link_state=%d code=0x%02x dlen=%u",
|
||||
link->etcp->log_name, link->link_state, pkt_code, pkt->data_len);
|
||||
memory_pool_free(e_sock->instance->pkt_pool, pkt);
|
||||
}
|
||||
```
|
||||
2. `src/transport_layer/etcp_api.c` (`etcp_int_recv`, после чтения `id`):
|
||||
```c
|
||||
uint8_t id = e->dgram[0];
|
||||
if (id == 0x01) DEBUG_WARN(DEBUG_CATEGORY_ETCP, "int_recv BGP id=0x01 conn=%s len=%zu", conn->log_name, e->len);
|
||||
```
|
||||
3. Android: `tools/chatgui-android/libutun_lite/instance_lite.c` добавлено
|
||||
`debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG);` (пересобрать APK: `cd tools/chatgui-android && ./build.sh`).
|
||||
4. chatgui конфиг `tools/chatgui/build/vibechat.cfg` — включены `bgp=debug`, `chat_sync=debug`, `member_sync=debug`, `debug=debug`.
|
||||
|
||||
Как читать (по таймстампу флапа):
|
||||
- есть `decrypt code=01 dlen=..` + `DROPPED` → потеря в проверке `link_state==CONNECTED`;
|
||||
- есть `decrypt` + нет `DROPPED` + нет `int_recv BGP` → потеря в `etcp_conn_input`/нормализаторе;
|
||||
- есть `int_recv BGP` + нет `BGP recv NODEINFO` → потеря в BGP-обработчике.
|
||||
|
||||
## Задача для агента
|
||||
|
||||
1. Пересобрать chatgui (`cd tools/chatgui && cmake --build build -j4`) и Android APK (`cd tools/chatgui-android && ./build.sh`).
|
||||
2. Запустить chatgui (DISPLAY=:0, `./vibechat`), воспроизвести флап реконнекта:
|
||||
```bash
|
||||
adb shell am force-stop com.utun.chat && adb shell am start -n com.utun.chat/.MainActivity
|
||||
```
|
||||
(повторить несколько раз; флап ловится не каждый раз).
|
||||
3. В логах (`tools/chatgui/build/chatgui.log`) найти потерю NODEINFO и **точно определить точку** по таблице выше.
|
||||
4. Починить первопричину (варианты):
|
||||
- буферизовать расшифрованный пакет до `LINK_STATE_CONNECTED` вместо `memory_pool_free`;
|
||||
- либо гарантированный BGP-retry (после conn UP, если узел пира не в `group->nodes` через N мс — повторно `topo_group_send_table_request`).
|
||||
5. Убрать весь диагностический мусор:
|
||||
- WARN "DROPPED" (etcp_connections.c), WARN "int_recv BGP" (etcp_api.c);
|
||||
- DEBUG-логи в `chat_member.c` (`on_member_props_changed`), `chat_sync.c` (`cs_on_peer_status_changed`), `memberlistmodel.cpp`;
|
||||
- `instance_lite.c` BGP-debug;
|
||||
- `vibechat.cfg` — вернуть категории в закомментированное состояние.
|
||||
6. Прогнать: connect→online, disconnect→offline, реконнект (флап)→online. `./check.sh` (или `cd src && make -j4`).
|
||||
|
||||
## Воспроизведение / ключевые артефакты
|
||||
|
||||
- Пир: Android SM-A525F (`com.utun.chat`), node_id `0x1657b3ea8281c0a8`.
|
||||
- Локальный: chatgui (`tools/chatgui/build/vibechat`), node_id `0x24cd036a6e659b9a`.
|
||||
- Канал: `ch_id=7206723622466219923`, `group_id=0x64036cf3a783ef93`.
|
||||
- Лог chatgui: `tools/chatgui/build/chatgui.log` (debug_file из конфига).
|
||||
- Лог Android: `adb logcat -d | grep -iE "send_nodeinfo|NOT in registry|NODEINFO|DROPPED|int_recv"`.
|
||||
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
@@ -0,0 +1,39 @@
|
||||
uTun licensing
|
||||
==============
|
||||
|
||||
The original uTun source code, including its command-line, desktop and Android
|
||||
applications, is licensed under the GNU General Public License, version 3 only
|
||||
(GPL-3.0-only), with the additional permission for Wintun below.
|
||||
|
||||
uTun is free software: you can redistribute it and/or modify it under the terms
|
||||
of the GNU General Public License as published by the Free Software Foundation,
|
||||
version 3 of the License.
|
||||
|
||||
uTun is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
The complete GNU GPL version 3 is provided in LICENSE. Copyright remains with
|
||||
the respective authors. Third-party code and binaries retain their own licenses
|
||||
and copyright notices; see THIRD_PARTY_NOTICES and the notices in their files.
|
||||
|
||||
Additional permission under GNU GPL version 3 section 7
|
||||
------------------------------------------------------
|
||||
|
||||
If you modify this Program, or any covered work, by linking or combining it with
|
||||
Wintun (or a modified version of that library), containing parts covered by the
|
||||
terms of the Wintun Prebuilt Binaries License, the licensors of this Program
|
||||
grant you additional permission to convey the resulting work.
|
||||
|
||||
This permission applies to the original uTun code. It does not change the
|
||||
license of Wintun or grant rights to modify its prebuilt binaries. Wintun must
|
||||
be used and distributed in accordance with its own license, through its
|
||||
Permitted API. No exception is granted for any other library.
|
||||
|
||||
Distribution
|
||||
------------
|
||||
|
||||
Include LICENSE, LICENSING and THIRD_PARTY_NOTICES with source and binary
|
||||
distributions. For GPL-covered binaries, also provide Corresponding Source
|
||||
as required by GNU GPL version 3; including these documents does not replace
|
||||
that requirement. Preserve upstream notices when updating bundled libraries.
|
||||
+16
@@ -3,6 +3,22 @@
|
||||
# Order matters: lib -> src -> tests (tests depend on src objects)
|
||||
SUBDIRS = lib lib/libopus src tests
|
||||
|
||||
# Полный комплект лицензий устанавливается вместе с бинарником и входит в make dist.
|
||||
licensedir = $(datadir)/licenses/utun
|
||||
dist_license_DATA = LICENSE LICENSING THIRD_PARTY_NOTICES
|
||||
|
||||
EXTRA_DIST = readme.md \
|
||||
doc/nat_detection.txt \
|
||||
doc/stun_nat.md \
|
||||
doc/node_snapshot.md \
|
||||
tinycrypt/LICENSE \
|
||||
tools/chatgui/zxing-cpp/LICENSE \
|
||||
tools/chatgui/zxing-cpp/core/src/libzueci/LICENSE \
|
||||
tools/chatgui/third_party/qhotkey/LICENSE \
|
||||
tools/chatgui-android/app/src/main/cpp/onnxruntime/LICENSE \
|
||||
tools/chatgui-android/app/src/main/cpp/onnxruntime/ThirdPartyNotices.txt \
|
||||
tools/chatgui-android/app/src/main/cpp/openssl/LICENSE.txt
|
||||
|
||||
# Clean build directory
|
||||
clean-local:
|
||||
-rm -rf build/
|
||||
@@ -1,3 +1,5 @@
|
||||
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](src/routing_layer/etcp_router_doc.md).
|
||||
|
||||
# План: эпоха (reset_id) для ETCP Router
|
||||
|
||||
Аналог фикса `reset_id` из ETCP (см. `61c572c`), применённый к сервисному слою
|
||||
|
||||
+11786
File diff suppressed because it is too large.
Load diff
+9
-3
@@ -153,10 +153,14 @@ general=info # общая информация
|
||||
```ini
|
||||
[firewall]
|
||||
allow=all # разрешить всё (bypass)
|
||||
allow=192.168.1.0/24 # разрешить подсеть
|
||||
allow=192.168.1.10 # разрешить весь IP
|
||||
allow=10.0.0.1:80 # разрешить конкретный IP:порт
|
||||
```
|
||||
|
||||
Фильтр основного TUN проверяет IP и TCP/UDP-порт назначения одинаково в обоих направлениях.
|
||||
Без разрешений весь трафик этого интерфейса запрещён; ответы требуют отдельных разрешений.
|
||||
CIDR-подсети не поддерживаются. Ошибочное правило отменяет загрузку нового конфига.
|
||||
|
||||
### 3.7. `[nat]` — EIM NAT
|
||||
|
||||
```ini
|
||||
@@ -553,11 +557,13 @@ dump=trace
|
||||
```ini
|
||||
[firewall]
|
||||
allow=all # всё разрешено
|
||||
allow=10.0.0.0/8 # разрешить подсеть
|
||||
allow=10.0.0.5 # разрешить весь IP
|
||||
allow=192.168.1.5:22 # разрешить IP:порт
|
||||
```
|
||||
|
||||
По умолчанию без секции `[firewall]` — фильтрация выключена.
|
||||
Без секции `[firewall]` весь входящий и исходящий трафик основного TUN запрещён.
|
||||
`allow=IP` и `allow=IP:0` разрешают все протоколы IP; `allow=IP:port` — TCP/UDP указанного порта.
|
||||
Для фрагментов IPv4 нужен весь IP либо `allow=all`. NAT и прокси используют отдельные TUN интерфейсы.
|
||||
|
||||
### 7.3. Проброс портов
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@echo off
|
||||
setlocal EnableExtensions
|
||||
echo uTun Build Script for MSYS2 UCRT64
|
||||
echo.
|
||||
|
||||
@@ -10,11 +11,11 @@ rem export https_proxy="http://192.168.29.1:9999"
|
||||
|
||||
|
||||
REM Default MSYS2 path
|
||||
set MSYS2_PATH=C:\msys64
|
||||
set "MSYS2_PATH=C:\msys64"
|
||||
|
||||
REM Use environment variable if set
|
||||
if not "%MSYS2_ROOT%"=="" set MSYS2_PATH=%MSYS2_ROOT%
|
||||
if not "%MSYS2%"=="" set MSYS2_PATH=%MSYS2%
|
||||
if not "%MSYS2_ROOT%"=="" set "MSYS2_PATH=%MSYS2_ROOT%"
|
||||
if not "%MSYS2%"=="" set "MSYS2_PATH=%MSYS2%"
|
||||
|
||||
REM Check if msys2_shell.cmd exists
|
||||
if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
|
||||
@@ -22,10 +23,15 @@ if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
|
||||
echo Please install MSYS2 from https://www.msys2.org/
|
||||
echo or set MSYS2_ROOT environment variable.
|
||||
pause
|
||||
popd
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Launch MSYS2 UCRT64 shell and run build script
|
||||
echo Starting MSYS2 UCRT64 environment...
|
||||
rem call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -c "bash build.sh > build_win.log 2>&1"
|
||||
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; bash build.sh 2>&1 | tee build_win.log"
|
||||
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; bash build.sh --installer 2>&1 | tee build_win.log"
|
||||
set "BUILD_RESULT=%ERRORLEVEL%"
|
||||
if not "%BUILD_RESULT%"=="0" echo [ERROR] Build or installer failed. See build_win.log.
|
||||
popd
|
||||
endlocal & exit /b %BUILD_RESULT%
|
||||
@@ -1,208 +1,221 @@
|
||||
#!/bin/bash
|
||||
# Build script for uTun (supports both Linux and Windows/MSYS2)
|
||||
# Usage: ./build.sh [options]
|
||||
# Options:
|
||||
# -h, --help Show this help
|
||||
# --clean Clean before build
|
||||
# --full Full rebuild (autoreconf + configure + make)
|
||||
# --asan Build with AddressSanitizer (debug)
|
||||
# --hardened Build with compiler security hardening flags
|
||||
# -j[N] Build with N parallel jobs (default: 4)
|
||||
#!/usr/bin/env bash
|
||||
# Инкрементальная сборка Linux, FreeBSD и Windows/MSYS2; флаги задаются через configure.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Detect platform
|
||||
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ -n "$MSYSTEM" ]]; then
|
||||
IS_WINDOWS=1
|
||||
echo "Detected Windows/MSYS2 environment"
|
||||
else
|
||||
IS_WINDOWS=0
|
||||
echo "Detected Linux environment"
|
||||
fi
|
||||
|
||||
# Parse arguments
|
||||
MODE=normal
|
||||
CLEAN=0
|
||||
FULL_REBUILD=0
|
||||
USE_ASAN=0
|
||||
USE_HARDENED=0
|
||||
JOBS="-j4"
|
||||
MAKE_ARGS=""
|
||||
HARDEN_FLAGS=""
|
||||
FULL=0
|
||||
BUILD_GUI=1
|
||||
BUILD_INSTALLER=0
|
||||
JOBS=4
|
||||
MAKE_ARGS=()
|
||||
MAKE_SETTINGS=()
|
||||
STATE=.utun-build-state
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
echo "Usage: ./build.sh [options]"
|
||||
echo "Options:"
|
||||
echo " -h, --help Show this help"
|
||||
echo " --clean Clean before build"
|
||||
echo " --full Full rebuild (autoreconf + configure + make)"
|
||||
echo " --asan Build with AddressSanitizer"
|
||||
echo " --hardened Build with compiler security hardening flags"
|
||||
echo " -j[N] Build with N parallel jobs (default: 4)"
|
||||
exit 0
|
||||
;;
|
||||
--clean)
|
||||
CLEAN=1
|
||||
shift
|
||||
;;
|
||||
--full)
|
||||
FULL_REBUILD=1
|
||||
shift
|
||||
;;
|
||||
--asan)
|
||||
USE_ASAN=1
|
||||
shift
|
||||
;;
|
||||
--hardened)
|
||||
USE_HARDENED=1
|
||||
shift
|
||||
;;
|
||||
-j*)
|
||||
JOBS="$1"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
MAKE_ARGS="$MAKE_ARGS $1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check required tools
|
||||
if ! command -v gcc &> /dev/null; then
|
||||
echo "Error: gcc not found."
|
||||
if [ "$IS_WINDOWS" -eq 1 ]; then
|
||||
echo "Please install: pacman -S mingw-w64-ucrt-x86_64-toolchain"
|
||||
else
|
||||
echo "Please install build-essential or gcc"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean if requested
|
||||
if [ "$CLEAN" -eq 1 ]; then
|
||||
echo "Cleaning build..."
|
||||
make clean 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Full rebuild: regenerate build system
|
||||
if [ "$FULL_REBUILD" -eq 1 ]; then
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "FULL REBUILD: autoreconf"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Check for autotools
|
||||
if ! command -v autoreconf &> /dev/null; then
|
||||
echo "Error: autoreconf not found."
|
||||
if [ "$IS_WINDOWS" -eq 1 ]; then
|
||||
echo "Install autotools: pacman -S mingw-w64-ucrt-x86_64-autotools"
|
||||
else
|
||||
echo "Install autotools: apt-get install autoconf automake libtool"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean generated files
|
||||
echo "Cleaning generated files..."
|
||||
make distclean 2>/dev/null || true
|
||||
rm -rf build/
|
||||
|
||||
# Regenerate build system (root Makefile is hand-written, not generated)
|
||||
echo ""
|
||||
echo "Running autoreconf -fi..."
|
||||
autoreconf -fi 2>&1 | tee autoreconf.log
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Build system regenerated successfully"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
else
|
||||
# Quick/incremental build: generate configure if needed
|
||||
if [ ! -f ./configure ]; then
|
||||
echo "Configure script not found. Generating..."
|
||||
if ! command -v autoreconf &> /dev/null; then
|
||||
echo "Error: autoreconf not found."
|
||||
if [ "$IS_WINDOWS" -eq 1 ]; then
|
||||
echo "Install autotools: pacman -S mingw-w64-ucrt-x86_64-autotools"
|
||||
else
|
||||
echo "Install autotools: apt-get install autoconf automake libtool"
|
||||
echo "Usage: ./build.sh [--clean] [--full] [--asan|--hardened] [--no-gui] [--installer] [-jN] [make arguments]"
|
||||
echo " --clean Rebuild objects without forcing configure"
|
||||
echo " --full Regenerate, configure and rebuild everything"
|
||||
echo " --asan AddressSanitizer; skip tests"
|
||||
echo " --hardened Enable compiler security hardening"
|
||||
echo " --no-gui Skip the separately configured ChatGUI"
|
||||
echo " --installer Build Windows ChatGUI in Release and create its Inno Setup installer"
|
||||
echo " -jN, -j N Parallel jobs (default: 4)"
|
||||
echo " CC/CFLAGS/CPPFLAGS/LDFLAGS/LIBS assignments are passed to configure"
|
||||
exit 0 ;;
|
||||
--clean) CLEAN=1 ;;
|
||||
--full) FULL=1 ;;
|
||||
--no-gui) BUILD_GUI=0 ;;
|
||||
--installer) BUILD_INSTALLER=1 ;;
|
||||
--asan|--hardened)
|
||||
selected_mode=${1#--}
|
||||
if [[ "$MODE" != normal && "$MODE" != "$selected_mode" ]]; then
|
||||
echo "ERROR: --asan and --hardened cannot be combined" >&2; exit 1
|
||||
fi
|
||||
exit 1
|
||||
MODE=$selected_mode ;;
|
||||
-j)
|
||||
[[ $# -gt 1 ]] || { echo "ERROR: -j requires a positive job count" >&2; exit 1; }
|
||||
shift; JOBS=$1 ;;
|
||||
-j*) JOBS=${1#-j} ;;
|
||||
CC=*|CFLAGS=*|CPPFLAGS=*|LDFLAGS=*|LIBS=*) export "$1" ;;
|
||||
CC+=*|CFLAGS+=*|CPPFLAGS+=*|LDFLAGS+=*|LIBS+=*)
|
||||
echo "ERROR: use NAME=value for configure flags; append assignments are not supported" >&2; exit 1 ;;
|
||||
*)
|
||||
MAKE_ARGS+=("$1")
|
||||
[[ "$1" != *=* ]] || MAKE_SETTINGS+=("$1") ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
[[ "$JOBS" =~ ^[1-9][0-9]*$ ]] || { echo "ERROR: invalid job count: $JOBS" >&2; exit 1; }
|
||||
if [[ "$BUILD_INSTALLER" = 1 ]]; then
|
||||
[[ "$BUILD_GUI" = 1 && "$MODE" = normal ]] || { echo "ERROR: --installer requires GUI and normal build mode" >&2; exit 1; }
|
||||
bash tools/chatgui/package_win.sh --check
|
||||
fi
|
||||
|
||||
PLATFORM=$(uname -s)
|
||||
MAKE_TOOL=${MAKE:-make}
|
||||
if [[ "$PLATFORM" = FreeBSD ]]; then
|
||||
MAKE_TOOL=${MAKE:-gmake}
|
||||
export CC="${CC:-clang}"
|
||||
fi
|
||||
MAKE_PATH=$(command -v "$MAKE_TOOL") || { echo "ERROR: GNU make not found: $MAKE_TOOL (FreeBSD: pkg install gmake)" >&2; exit 1; }
|
||||
MAKE_ID=$("$MAKE_TOOL" --version) || { echo "ERROR: GNU make is required; use MAKE=gmake on BSD" >&2; exit 1; }
|
||||
[[ "$MAKE_ID" = *'GNU Make'* ]] || { echo "ERROR: GNU make is required; use MAKE=gmake on BSD" >&2; exit 1; }
|
||||
export MAKE="$MAKE_TOOL"
|
||||
if command -v sha256sum >/dev/null; then
|
||||
HASH_COMMAND=(sha256sum)
|
||||
elif command -v sha256 >/dev/null; then
|
||||
HASH_COMMAND=(sha256 -r)
|
||||
elif command -v shasum >/dev/null; then
|
||||
HASH_COMMAND=(shasum -a 256)
|
||||
else
|
||||
echo "ERROR: SHA-256 utility not found: sha256sum, sha256 or shasum is required" >&2; exit 1
|
||||
fi
|
||||
read -r -a COMPILER <<< "${CC:-gcc}"
|
||||
COMPILER_PATH=$(command -v "${COMPILER[0]}") || { echo "ERROR: compiler not found: ${CC:-gcc}" >&2; exit 1; }
|
||||
COMPILER_ID=$("${COMPILER[@]}" --version) || { echo "ERROR: cannot identify compiler: ${CC:-gcc}" >&2; exit 1; }
|
||||
|
||||
# Хеши учитывают содержимое и имена файлов, включая добавление/удаление макросов.
|
||||
hash_files() {
|
||||
local result
|
||||
result=$({ printf '%s\0' "$@"; "${HASH_COMMAND[@]}" "$@"; } | "${HASH_COMMAND[@]}")
|
||||
printf '%s\n' "${result%% *}"
|
||||
}
|
||||
|
||||
BUILD_DIRS=(lib lib/libopus src tests)
|
||||
AUTOTOOLS_INPUTS=(configure.ac Makefile.am lib/Makefile.am lib/libopus/Makefile.am src/Makefile.am tests/Makefile.am)
|
||||
AUTOTOOLS_OUTPUTS=(configure aclocal.m4 config.h.in Makefile.in lib/Makefile.in lib/libopus/Makefile.in src/Makefile.in tests/Makefile.in)
|
||||
CONFIG_OUTPUTS=(config.status config.h Makefile lib/Makefile lib/libopus/Makefile src/Makefile tests/Makefile)
|
||||
shopt -s globstar nullglob
|
||||
for macro in m4/**/*.m4; do AUTOTOOLS_INPUTS+=("$macro"); done
|
||||
GENERATION_INPUT=$(hash_files "${AUTOTOOLS_INPUTS[@]}")
|
||||
GENERATION_OUTPUT=missing
|
||||
CONFIG_OUTPUT=missing
|
||||
missing_generation=0
|
||||
missing_configuration=0
|
||||
for file in "${AUTOTOOLS_OUTPUTS[@]}"; do [[ -f "$file" ]] || missing_generation=1; done
|
||||
for file in "${CONFIG_OUTPUTS[@]}"; do [[ -f "$file" ]] || missing_configuration=1; done
|
||||
if [[ "$missing_generation" = 0 ]]; then GENERATION_OUTPUT=$(hash_files "${AUTOTOOLS_OUTPUTS[@]}"); fi
|
||||
if [[ "$missing_configuration" = 0 ]]; then CONFIG_OUTPUT=$(hash_files "${CONFIG_OUTPUTS[@]}"); fi
|
||||
|
||||
FFMPEG_PACKAGES=(libavformat libavcodec libavutil libswscale libswresample)
|
||||
FFMPEG_OPTION=--without-ffmpeg
|
||||
FFMPEG_ID=unavailable
|
||||
FFMPEG_CFLAGS=""
|
||||
FFMPEG_LDFLAGS=""
|
||||
if command -v pkg-config >/dev/null && pkg-config --exists "${FFMPEG_PACKAGES[@]}"; then
|
||||
FFMPEG_OPTION=--with-ffmpeg
|
||||
FFMPEG_ID=$(pkg-config --modversion "${FFMPEG_PACKAGES[@]}")
|
||||
FFMPEG_CFLAGS=$(pkg-config --cflags "${FFMPEG_PACKAGES[@]}")
|
||||
FFMPEG_LDFLAGS=$(pkg-config --libs-only-L "${FFMPEG_PACKAGES[@]}")
|
||||
fi
|
||||
request_sum=$({
|
||||
printf '%s\0' "$MODE" "$PLATFORM" "${MSYSTEM:-}" "${MSYSTEM_PREFIX:-}" "${CC:-gcc}" "$COMPILER_PATH" "$COMPILER_ID"
|
||||
printf '%s\0' "$MAKE_PATH" "$MAKE_ID" "${HASH_COMMAND[@]}"
|
||||
printf '%s\0' "${CFLAGS+x}" "${CFLAGS-}" "${CPPFLAGS+x}" "${CPPFLAGS-}" "${LDFLAGS+x}" "${LDFLAGS-}"
|
||||
printf '%s\0' "${LIBS+x}" "${LIBS-}" "${MAKEFLAGS-}" "${DEBUG_FLAGS+x}" "${DEBUG_FLAGS-}"
|
||||
printf '%s\0' "$FFMPEG_OPTION" "$FFMPEG_ID" "$FFMPEG_CFLAGS" "$FFMPEG_LDFLAGS"
|
||||
printf '%s\0' "${MAKE_SETTINGS[@]}"
|
||||
hash_files build.sh
|
||||
} | "${HASH_COMMAND[@]}")
|
||||
CONFIG_REQUEST=${request_sum%% *}
|
||||
|
||||
OLD_GENERATION_INPUT=""; OLD_GENERATION_OUTPUT=""; OLD_CONFIG_REQUEST=""; OLD_CONFIG_OUTPUT=""
|
||||
if [[ -f "$STATE" ]]; then
|
||||
if ! {
|
||||
read -r OLD_GENERATION_INPUT; read -r OLD_GENERATION_OUTPUT
|
||||
read -r OLD_CONFIG_REQUEST; read -r OLD_CONFIG_OUTPUT
|
||||
} < "$STATE"; then
|
||||
echo "WARN: incomplete build state; adopting the build with a clean rebuild"
|
||||
OLD_GENERATION_INPUT=""; OLD_GENERATION_OUTPUT=""; OLD_CONFIG_REQUEST=""; OLD_CONFIG_OUTPUT=""
|
||||
fi
|
||||
fi
|
||||
|
||||
REGENERATE=0
|
||||
RECONFIGURE=0
|
||||
if [[ "$FULL" = 1 || "$GENERATION_INPUT" != "$OLD_GENERATION_INPUT" || "$GENERATION_OUTPUT" != "$OLD_GENERATION_OUTPUT" ]]; then
|
||||
echo "INFO: regenerating Autotools: full=$FULL"
|
||||
if [[ "$GENERATION_INPUT" != "$OLD_GENERATION_INPUT" ]]; then echo "INFO: Autotools descriptions changed or unrecorded"; fi
|
||||
if [[ "$GENERATION_OUTPUT" != "$OLD_GENERATION_OUTPUT" ]]; then echo "INFO: generated Autotools files changed or missing"; fi
|
||||
command -v autoreconf >/dev/null || { echo "ERROR: autoreconf is required to regenerate the build" >&2; exit 1; }
|
||||
mkdir -p m4
|
||||
autoreconf -fi 2>&1 | tee autoreconf.log || { echo "ERROR: autoreconf failed; see autoreconf.log" >&2; exit 1; }
|
||||
REGENERATE=1
|
||||
fi
|
||||
if [[ "$REGENERATE" = 1 || "$CONFIG_REQUEST" != "$OLD_CONFIG_REQUEST" || "$CONFIG_OUTPUT" != "$OLD_CONFIG_OUTPUT" ]]; then
|
||||
echo "INFO: configuring: mode=$MODE regenerated=$REGENERATE"
|
||||
if [[ "$CONFIG_REQUEST" != "$OLD_CONFIG_REQUEST" ]]; then echo "INFO: compiler, flags or optional dependencies changed or unrecorded"; fi
|
||||
if [[ "$CONFIG_OUTPUT" != "$OLD_CONFIG_OUTPUT" ]]; then echo "INFO: configuration files changed or missing"; fi
|
||||
CONFIG_ARGS=("$FFMPEG_OPTION" --disable-hardening)
|
||||
if [[ "$MODE" = asan ]]; then
|
||||
export CFLAGS="${CFLAGS-} -g -Og -fsanitize=address -fno-omit-frame-pointer -fno-pie"
|
||||
export LDFLAGS="${LDFLAGS-} -fsanitize=address -no-pie"
|
||||
if [[ "$PLATFORM" = FreeBSD ]]; then export ASAN_OPTIONS="${ASAN_OPTIONS:-detect_leaks=0}"; fi
|
||||
elif [[ "$MODE" = hardened ]]; then
|
||||
CONFIG_ARGS+=(--enable-hardening)
|
||||
fi
|
||||
if [[ "$FFMPEG_OPTION" = --with-ffmpeg ]]; then
|
||||
export CPPFLAGS="${CPPFLAGS-} $FFMPEG_CFLAGS"
|
||||
export LDFLAGS="${LDFLAGS-} $FFMPEG_LDFLAGS"
|
||||
fi
|
||||
./configure "${CONFIG_ARGS[@]}" 2>&1 | tee configure.log || { echo "ERROR: configure failed; see configure.log" >&2; exit 1; }
|
||||
RECONFIGURE=1
|
||||
fi
|
||||
|
||||
# Проверяем только зависимости объектов, перечисленных Automake; посторонние сборки не затрагиваем.
|
||||
MISSING_DEPS=0
|
||||
for directory in "${BUILD_DIRS[@]}"; do
|
||||
while read -r keyword depfile marker; do
|
||||
[[ "$keyword" = include && "$marker" = '# am--include-marker' && "$depfile" = *.Po ]] || continue
|
||||
depfile=${depfile//\$\(DEPDIR\)/.deps}
|
||||
object=${depfile/.deps\//}
|
||||
object=${object%.Po}
|
||||
[[ -f "$directory/$object.o" || -f "$directory/$object.obj" ]] || continue
|
||||
dependency_line=""
|
||||
if [[ -s "$directory/$depfile" ]]; then IFS= read -r dependency_line < "$directory/$depfile" || true; fi
|
||||
if [[ "$dependency_line" != *:* ]]; then
|
||||
echo "WARN: missing or empty dependencies for $directory/$object: $directory/$depfile"
|
||||
MISSING_DEPS=1
|
||||
break
|
||||
fi
|
||||
autoreconf -fiv
|
||||
fi
|
||||
fi
|
||||
|
||||
# Auto-detect FFmpeg (optional video transcoding). If libav libraries are
|
||||
# available, build with --with-ffmpeg; configure gracefully skips it otherwise.
|
||||
FFMPEG_OPT=""
|
||||
if command -v pkg-config >/dev/null 2>&1 && \
|
||||
pkg-config --exists libavformat libavcodec libavutil libswscale libswresample 2>/dev/null; then
|
||||
FFMPEG_OPT="--with-ffmpeg"
|
||||
echo "FFmpeg detected — building with video transcoding"
|
||||
done < "$directory/Makefile"
|
||||
done
|
||||
if [[ "$RECONFIGURE" = 1 || "$CLEAN" = 1 || "$MISSING_DEPS" = 1 ]]; then
|
||||
echo "INFO: cleaning objects: reconfigured=$RECONFIGURE requested=$CLEAN missing_dependencies=$MISSING_DEPS"
|
||||
"$MAKE_TOOL" clean 2>&1 | tee build.log || { echo "ERROR: clean failed; see build.log" >&2; exit 1; }
|
||||
else
|
||||
echo "FFmpeg not found — building without video transcoding"
|
||||
echo "INFO: configuration unchanged; incremental build (mode=$MODE)"
|
||||
: > build.log
|
||||
fi
|
||||
|
||||
# Generate Makefiles if needed
|
||||
if [ ! -f Makefile ]; then
|
||||
echo ""
|
||||
echo "Running ./configure..."
|
||||
if [ "$USE_ASAN" -eq 1 ]; then
|
||||
./configure CFLAGS="-g -Og -fsanitize=address -fno-omit-frame-pointer -fno-pie" LDFLAGS="-no-pie" $FFMPEG_OPT
|
||||
elif [ "$USE_HARDENED" -eq 1 ]; then
|
||||
./configure --enable-hardening $FFMPEG_OPT
|
||||
echo "INFO: compiling: mode=$MODE jobs=$JOBS"
|
||||
if [[ "$MODE" = asan ]]; then
|
||||
echo "INFO: AddressSanitizer: building lib, Opus and src; skipping tests"
|
||||
MAKE_ARGS+=("SUBDIRS=lib lib/libopus src")
|
||||
fi
|
||||
"$MAKE_TOOL" "-j$JOBS" "${MAKE_ARGS[@]}" 2>&1 | tee -a build.log || { echo "ERROR: build failed; see build.log" >&2; exit 1; }
|
||||
|
||||
# Состояние обновляется после успешной сборки ядра; ошибка GUI не инвалидирует готовые объекты.
|
||||
GENERATION_OUTPUT=$(hash_files "${AUTOTOOLS_OUTPUTS[@]}")
|
||||
CONFIG_OUTPUT=$(hash_files "${CONFIG_OUTPUTS[@]}")
|
||||
state_tmp="$STATE.tmp.$$"
|
||||
trap 'rm -f "$state_tmp"' EXIT
|
||||
printf '%s\n' "$GENERATION_INPUT" "$GENERATION_OUTPUT" "$CONFIG_REQUEST" "$CONFIG_OUTPUT" > "$state_tmp"
|
||||
mv -f "$state_tmp" "$STATE"
|
||||
|
||||
if [[ "$BUILD_GUI" = 1 && ( "$BUILD_INSTALLER" = 1 || -f tools/chatgui/build/CMakeCache.txt ) ]]; then
|
||||
command -v cmake >/dev/null || { echo "ERROR: ChatGUI exists but cmake is not available" >&2; exit 1; }
|
||||
echo "INFO: building ChatGUI with its CMake configuration: jobs=$JOBS"
|
||||
if [[ "${MSYSTEM:-}" = UCRT64 ]]; then
|
||||
GUI_ARGS=("-j$JOBS")
|
||||
[[ "$BUILD_INSTALLER" != 1 ]] || GUI_ARGS+=(--installer)
|
||||
(cd tools/chatgui && bash build.sh "${GUI_ARGS[@]}") || { echo "ERROR: ChatGUI build failed" >&2; exit 1; }
|
||||
else
|
||||
./configure $FFMPEG_OPT
|
||||
cmake --build tools/chatgui/build --parallel "$JOBS" || { echo "ERROR: ChatGUI build failed" >&2; exit 1; }
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build (all .o files go to build/ directory, binary copied to project root)
|
||||
echo ""
|
||||
echo "Compiling with $JOBS..."
|
||||
BUILD_SUCCESS=0
|
||||
if [ "$USE_ASAN" -eq 1 ]; then
|
||||
echo "(AddressSanitizer enabled — lib/ and src/ only, skipping tests)"
|
||||
AF="-fsanitize=address -fno-omit-frame-pointer -g -Og"
|
||||
AL="-fsanitize=address"
|
||||
(cd lib && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$AF" "LDFLAGS+=$AL" $JOBS $MAKE_ARGS 2>&1) || { echo "lib build failed"; exit 1; }
|
||||
(cd src && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$AF" "LDFLAGS+=$AL" $JOBS $MAKE_ARGS 2>&1) || { echo "src build failed"; exit 1; }
|
||||
BUILD_SUCCESS=1
|
||||
elif [ "$USE_HARDENED" -eq 1 ]; then
|
||||
echo "(Hardening flags enabled)"
|
||||
HF="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fstack-clash-protection -Wformat=2 -Wformat-overflow=2 -fPIE"
|
||||
HL="-Wl,-z,relro -Wl,-z,now -pie"
|
||||
(cd lib && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$HF" "LDFLAGS+=$HL" $JOBS $MAKE_ARGS 2>&1) || { echo "lib build failed"; exit 1; }
|
||||
(cd src && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$HF" "LDFLAGS+=$HL" $JOBS $MAKE_ARGS 2>&1) || { echo "src build failed"; exit 1; }
|
||||
(cd tests && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$HF" "LDFLAGS+=$HL" $JOBS $MAKE_ARGS 2>&1) || { echo "tests build failed"; exit 1; }
|
||||
BUILD_SUCCESS=1
|
||||
else
|
||||
make $JOBS $MAKE_ARGS 2>&1 | tee build.log
|
||||
if [ ${PIPESTATUS[0]} -eq 0 ]; then
|
||||
BUILD_SUCCESS=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $BUILD_SUCCESS -eq 1 ]; then
|
||||
echo ""
|
||||
echo "Build completed successfully!"
|
||||
echo "Binary: utun"
|
||||
|
||||
# Rebuild chatgui (CMake project) if transport sources changed
|
||||
if [ -f tools/chatgui/build/CMakeCache.txt ] && command -v cmake &> /dev/null; then
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Rebuilding chatgui (CMake)"
|
||||
echo "======================================"
|
||||
(cd tools/chatgui/build && cmake --build . -j4 2>&1) || echo "WARNING: chatgui build failed"
|
||||
fi
|
||||
else
|
||||
echo ""
|
||||
echo "ERROR: Build failed!"
|
||||
echo "Check build.log for details"
|
||||
exit 1
|
||||
fi
|
||||
echo "Build completed successfully!"
|
||||
echo "Binary: src/utun"
|
||||
+24
-136
@@ -1,144 +1,32 @@
|
||||
#!/bin/sh
|
||||
# Build script for uTun on FreeBSD (full rebuild)
|
||||
# Usage: ./build_bsd_full.sh [options]
|
||||
# Options:
|
||||
# -h, --help Show this help
|
||||
# --asan Build with AddressSanitizer (clang)
|
||||
# --hardened Build with compiler security hardening flags (clang)
|
||||
# -j[N] Build with N parallel jobs (default: sysctl hw.ncpu)
|
||||
|
||||
# Полная сборка FreeBSD: зависимости и общий инкрементальный скрипт.
|
||||
set -e
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
case " ${*} " in
|
||||
*" --help "*|*" -h "*)
|
||||
echo "Usage: ./build_bsd_full.sh [--asan|--hardened] [-jN] [build.sh arguments]"
|
||||
echo "Installs missing build tools and runs build.sh --full"
|
||||
exit 0 ;;
|
||||
esac
|
||||
|
||||
# Parse arguments
|
||||
USE_ASAN=0
|
||||
USE_HARDENED=0
|
||||
JOBS=""
|
||||
MAKE_ARGS=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
echo "Usage: ./build_bsd_full.sh [options]"
|
||||
echo "Options:"
|
||||
echo " -h, --help Show this help"
|
||||
echo " --asan Build with AddressSanitizer"
|
||||
echo " --hardened Build with compiler security hardening flags"
|
||||
echo " -j[N] Build with N parallel jobs (default: sysctl hw.ncpu)"
|
||||
exit 0
|
||||
;;
|
||||
--asan)
|
||||
USE_ASAN=1
|
||||
shift
|
||||
;;
|
||||
--hardened)
|
||||
USE_HARDENED=1
|
||||
shift
|
||||
;;
|
||||
-j*)
|
||||
JOBS="$1"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
MAKE_ARGS="$MAKE_ARGS $1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
need_install=0
|
||||
for tool in bash gmake autoreconf aclocal automake libtoolize pkg-config; do
|
||||
command -v "$tool" >/dev/null 2>&1 || need_install=1
|
||||
done
|
||||
|
||||
if [ -z "$JOBS" ]; then
|
||||
if command -v nproc >/dev/null 2>&1; then
|
||||
JOBS="-j$(nproc)"
|
||||
else
|
||||
JOBS="-j$(sysctl -n hw.ncpu)"
|
||||
fi
|
||||
if [ "$need_install" -eq 1 ]; then
|
||||
echo "INFO: installing missing FreeBSD build dependencies (requires root)"
|
||||
pkg install -y bash gmake autoconf automake libtool pkgconf openssl || {
|
||||
echo "ERROR: cannot install FreeBSD build dependencies" >&2; exit 1;
|
||||
}
|
||||
fi
|
||||
|
||||
# Проверка и установка зависимостей
|
||||
need_install=""
|
||||
|
||||
for cmd in autoreconf aclocal automake libtoolize; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
need_install=1
|
||||
break
|
||||
fi
|
||||
jobs_found=0
|
||||
for argument do
|
||||
case "$argument" in -j*) jobs_found=1 ;; esac
|
||||
done
|
||||
|
||||
if [ -n "$need_install" ]; then
|
||||
echo "Устанавливаются необходимые пакеты (нужен root)..."
|
||||
pkg update
|
||||
pkg install -y autoconf automake libtool pkgconf openssl
|
||||
fi
|
||||
|
||||
# Предпочитаем GNU make (gmake), но fallback на bmake — он собирает
|
||||
# autotools-генерённые Makefile данного проекта. gmake ставится отдельно:
|
||||
# pkg install gmake (нужен root)
|
||||
GMAKE="$(command -v gmake || command -v make)"
|
||||
echo "Используем make: $GMAKE"
|
||||
|
||||
mkdir -p m4
|
||||
|
||||
echo "=== Полная пересборка uTun ==="
|
||||
|
||||
# 1. Очистка
|
||||
echo "[1/4] Очистка..."
|
||||
$GMAKE distclean 2>/dev/null || true
|
||||
rm -f config.h config.status config.log
|
||||
rm -f configure.log autoreconf.log build.log
|
||||
|
||||
# 2. Генерация configure
|
||||
echo "[2/4] Автогенерация..."
|
||||
autoreconf -fi 2>&1 | tee autoreconf.log
|
||||
|
||||
# Проверяем, появился ли configure
|
||||
if [ ! -f configure ]; then
|
||||
echo "Ошибка: autoreconf не создал configure."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod +x configure
|
||||
|
||||
# 3. Конфигурация
|
||||
echo "[3/4] Конфигурация..."
|
||||
|
||||
# FFmpeg (опционально) — автоопределение через pkg-config
|
||||
FFMPEG_OPT=""
|
||||
if command -v pkg-config >/dev/null 2>&1 && \
|
||||
pkg-config --exists libavformat libavcodec libavutil libswscale libswresample 2>/dev/null; then
|
||||
FFMPEG_OPT="--with-ffmpeg"
|
||||
echo "FFmpeg detected — building with video transcoding"
|
||||
else
|
||||
echo "FFmpeg not found — building without video transcoding"
|
||||
fi
|
||||
|
||||
if [ "$USE_ASAN" -eq 1 ]; then
|
||||
echo "(AddressSanitizer enabled — clang, lib/ + src/ only)"
|
||||
./configure --prefix=/usr/local CC=clang \
|
||||
CFLAGS="-g -O1 -fsanitize=address -fno-omit-frame-pointer" \
|
||||
LDFLAGS="-fsanitize=address" $FFMPEG_OPT 2>&1 | tee configure.log
|
||||
elif [ "$USE_HARDENED" -eq 1 ]; then
|
||||
echo "(Hardening enabled — clang-safe flags)"
|
||||
./configure --prefix=/usr/local CC=clang \
|
||||
CFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fPIE" \
|
||||
LDFLAGS="-Wl,-z,relro -Wl,-z,now -pie" $FFMPEG_OPT 2>&1 | tee configure.log
|
||||
else
|
||||
./configure --prefix=/usr/local $FFMPEG_OPT 2>&1 | tee configure.log
|
||||
fi
|
||||
|
||||
# 4. Сборка
|
||||
echo "[4/4] Сборка..."
|
||||
if [ "$USE_ASAN" -eq 1 ]; then
|
||||
(cd lib && $GMAKE $JOBS $MAKE_ARGS 2>&1 | tee ../build.log)
|
||||
(cd lib/libopus && $GMAKE $JOBS $MAKE_ARGS 2>&1 | tee ../../build.log)
|
||||
(cd src && $GMAKE $JOBS $MAKE_ARGS 2>&1 | tee ../build.log)
|
||||
else
|
||||
$GMAKE $JOBS $MAKE_ARGS 2>&1 | tee build.log
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=== Готово ==="
|
||||
echo "Бинарный файл: src/utun"
|
||||
if [ "$USE_ASAN" -eq 1 ]; then
|
||||
echo
|
||||
echo "Запуск с ASAN (detect_leaks=0 обязателен на FreeBSD):"
|
||||
echo " ASAN_OPTIONS=detect_leaks=0:halt_on_error=0 ./src/utun -f -p /tmp/utun.pid -l /tmp/utun.log"
|
||||
if [ "$jobs_found" -eq 0 ]; then
|
||||
jobs=$(sysctl -n hw.ncpu) || { echo "ERROR: cannot determine CPU count" >&2; exit 1; }
|
||||
set -- "-j$jobs" "$@"
|
||||
fi
|
||||
echo "INFO: running shared build script for FreeBSD"
|
||||
exec bash "$SCRIPT_DIR/build.sh" --full "$@"
|
||||
+1
-1
@@ -70,7 +70,7 @@ echo "Building utun.exe..."
|
||||
cd src
|
||||
EXE_NEEDS_REBUILD=0
|
||||
|
||||
CORE_SOURCES="utun.c utun_instance.c config_parser.c config_updater.c routing_layer/route_lib.c topo_bgp.c routing_layer/routing.c tun_if.c tun_route.c transport_layer/etcp.c transport_layer/etcp_connections.c transport_layer/etcp_loadbalancer.c transport_layer/secure_channel.c transport_layer/crc32.c transport_layer/pkt_normalizer.c transport_layer/etcp_api.c tun_windows.c"
|
||||
CORE_SOURCES="utun.c utun_instance.c config_parser.c config_updater.c routing_layer/route_lib.c topo_bgp.c routing_layer/routing.c tun_if.c tun_route.c transport_layer/etcp.c transport_layer/etcp_padding.c transport_layer/reality_record.c transport_layer/reality_io.c transport_layer/etcp_session.c transport_layer/etcp_connections.c transport_layer/etcp_loadbalancer.c transport_layer/secure_channel.c transport_layer/crc32.c transport_layer/pkt_normalizer.c transport_layer/etcp_api.c tun_windows.c"
|
||||
|
||||
for src in $CORE_SOURCES; do
|
||||
if [ -f "$src" ]; then
|
||||
|
||||
+11
-5
@@ -1,4 +1,5 @@
|
||||
@echo off
|
||||
setlocal EnableExtensions
|
||||
echo uTun Full Build Script for MSYS2 UCRT64
|
||||
echo =========================================
|
||||
echo This will regenerate build system with autoreconf
|
||||
@@ -11,11 +12,11 @@ rem export https_proxy="http://192.168.29.1:9999"
|
||||
|
||||
|
||||
REM Default MSYS2 path
|
||||
set MSYS2_PATH=C:\msys64
|
||||
set "MSYS2_PATH=C:\msys64"
|
||||
|
||||
REM Use environment variable if set
|
||||
if not "%MSYS2_ROOT%"=="" set MSYS2_PATH=%MSYS2_ROOT%
|
||||
if not "%MSYS2%"=="" set MSYS2_PATH=%MSYS2%
|
||||
if not "%MSYS2_ROOT%"=="" set "MSYS2_PATH=%MSYS2_ROOT%"
|
||||
if not "%MSYS2%"=="" set "MSYS2_PATH=%MSYS2%"
|
||||
|
||||
REM Check if msys2_shell.cmd exists
|
||||
if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
|
||||
@@ -23,11 +24,16 @@ if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
|
||||
echo Please install MSYS2 from https://www.msys2.org/
|
||||
echo or set MSYS2_ROOT environment variable.
|
||||
pause
|
||||
popd
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Launch MSYS2 UCRT64 shell and run build script with --full flag
|
||||
echo Starting MSYS2 UCRT64 environment for FULL REBUILD...
|
||||
echo Running: bash build.sh --full
|
||||
echo Running: bash build.sh --full --installer
|
||||
echo.
|
||||
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; bash build.sh --full 2>&1 | tee build_full.log"
|
||||
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; bash build.sh --full --installer 2>&1 | tee build_full.log"
|
||||
set "BUILD_RESULT=%ERRORLEVEL%"
|
||||
if not "%BUILD_RESULT%"=="0" echo [ERROR] Build or installer failed. See build_full.log.
|
||||
popd
|
||||
endlocal & exit /b %BUILD_RESULT%
|
||||
@@ -16,7 +16,7 @@ set "TOTAL=0"
|
||||
if not exist "%LOGS_DIR%" mkdir "%LOGS_DIR%"
|
||||
|
||||
:: List of all test executables
|
||||
set "TEST_LIST=test_etcp_crypto test_crypto test_etcp_two_instances test_etcp_simple_traffic test_etcp_minimal test_etcp_100_packets test_pkt_normalizer_etcp test_pkt_normalizer_standalone test_etcp_api test_ll_queue test_ecc_encrypt test_intensive_memory_pool test_memory_pool_and_config test_packet_dump test_u_async_comprehensive test_u_async_performance test_debug_categories test_config_debug test_route_lib test_bgp_route_exchange bench_timeout_heap bench_uasync_timeouts"
|
||||
set "TEST_LIST=bench_timeout_heap bench_uasync_timeouts test_etcp_minimal test_u_async_performance test_packet_dump test_config_debug test_debug_categories test_memory_pool_and_config test_etcp_crypto test_route_lib test_crypto test_ecc_encrypt test_ll_queue test_intensive_memory_pool test_pkt_normalizer_standalone test_etcp_two_instances test_u_async_comprehensive test_etcp_simple_traffic test_pkt_normalizer_etcp test_etcp_100_packets test_etcp_api test_bgp_route_exchange"
|
||||
|
||||
echo Running tests...
|
||||
echo.
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
MAKE_TOOL=${MAKE:-$(command -v gmake || command -v make)}
|
||||
TIMEOUT_TOOL=$(command -v timeout || command -v gtimeout) || {
|
||||
echo "ERROR: timeout or gtimeout is required for the test runner" >&2; exit 1;
|
||||
}
|
||||
|
||||
LAST=0; [ -f .check_time ] && read -r LAST < .check_time
|
||||
TMO=$(( LAST + 30 ))
|
||||
@@ -8,21 +12,29 @@ TMO=$(( LAST + 30 ))
|
||||
|
||||
START=$(date +%s)
|
||||
TMP=$(mktemp)
|
||||
timeout $TMO make check -j4 > "$TMP" 2>&1 || {
|
||||
"$TIMEOUT_TOOL" "$TMO" "$MAKE_TOOL" check -j4 > "$TMP" 2>&1 || {
|
||||
tail -40 "$TMP"; rm -f "$TMP"
|
||||
[ $(date +%s) -ge $((START + TMO)) ] && echo "TIMEOUT (${TMO}s)" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -E '\[PASS|\[FAIL|\[SKIP|Test Results|Failed tests|Log directory|^====' "$TMP"
|
||||
grep -E '\[INFO|\[PASS|\[FAIL|\[SKIP|Test Results|Failed tests|Log directory|^====' "$TMP"
|
||||
rm -f "$TMP"
|
||||
|
||||
# Интеграционный тест tcp_proxy_full (требует sudo; без passwordless sudo — пропускаем)
|
||||
echo ""
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
make -C tests check-proxy
|
||||
if [ "$(uname -s)" != Linux ]; then
|
||||
echo "[SKIP] tcp_proxy_full integration tests require Linux networking tools"
|
||||
elif [ "$(id -u)" -eq 0 ]; then
|
||||
"$MAKE_TOOL" -C tests check-proxy
|
||||
"$MAKE_TOOL" -C tests check-burst
|
||||
"$MAKE_TOOL" -C tests check-load
|
||||
elif command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
|
||||
echo "=== tcp_proxy_full integration test (sudo) ==="
|
||||
sudo -n make -C tests check-proxy
|
||||
sudo -n "$MAKE_TOOL" -C tests check-proxy
|
||||
echo "=== tcp_proxy_full burst test (sudo) ==="
|
||||
sudo -n "$MAKE_TOOL" -C tests check-burst
|
||||
echo "=== tcp_proxy_full load test (sudo) ==="
|
||||
sudo -n "$MAKE_TOOL" -C tests check-load
|
||||
else
|
||||
echo "[SKIP] tcp_proxy_full integration test (requires passwordless sudo)"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
@echo off
|
||||
setlocal
|
||||
pushd "%~dp0"
|
||||
if errorlevel 1 (
|
||||
echo [ERROR] Cannot open the script directory.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set "MSYS2_PATH=C:\msys64"
|
||||
if not "%MSYS2_ROOT%"=="" set "MSYS2_PATH=%MSYS2_ROOT%"
|
||||
if not "%MSYS2%"=="" set "MSYS2_PATH=%MSYS2%"
|
||||
set "PROBE_LOG=%CD%\cc-probe.log"
|
||||
set "PROBE_RESULT=1"
|
||||
|
||||
> "%PROBE_LOG%" echo [INFO] MSYS2: %MSYS2_PATH%
|
||||
if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
|
||||
>> "%PROBE_LOG%" echo [ERROR] msys2_shell.cmd not found. Set MSYS2_ROOT to the MSYS2 directory.
|
||||
goto finish
|
||||
)
|
||||
|
||||
echo [INFO] Checking the UCRT64 C compiler and Opus build...
|
||||
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "bash ./check_cc.sh" >> "%PROBE_LOG%" 2>&1
|
||||
set "PROBE_RESULT=%ERRORLEVEL%"
|
||||
|
||||
:finish
|
||||
>> "%PROBE_LOG%" echo [INFO] MSYS2 shell exit code: %PROBE_RESULT%
|
||||
type "%PROBE_LOG%"
|
||||
echo.
|
||||
echo [INFO] Log: "%PROBE_LOG%"
|
||||
if not "%PROBE_RESULT%"=="0" echo [ERROR] Compiler or Opus check failed. See the log above.
|
||||
if /i not "%~1"=="--no-pause" pause
|
||||
popd
|
||||
exit /b %PROBE_RESULT%
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
#!/bin/bash
|
||||
set -uo pipefail
|
||||
|
||||
# Проверяем базовую компиляцию, затем Opus отдельно и через существующую CMake-сборку.
|
||||
probe_cc="${UTUN_PROBE_CC:-/ucrt64/bin/cc.exe}"
|
||||
printf '[INFO] MSYSTEM=%s\n[INFO] PATH=%s\n[INFO] Compiler=%s\n' "${MSYSTEM:-unset}" "$PATH" "$probe_cc"
|
||||
if [ ! -x "$probe_cc" ]; then
|
||||
printf '[ERROR] Compiler is missing or not executable: %s\n' "$probe_cc"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
probe_dir=$(mktemp -d "${TMPDIR:-/tmp}/utun-cc-probe.XXXXXX") || {
|
||||
printf '[ERROR] Cannot create the temporary probe directory.\n'
|
||||
exit 1
|
||||
}
|
||||
trap 'rm -rf -- "$probe_dir" || printf "[WARN] Cannot remove probe directory: %s\n" "$probe_dir"' EXIT
|
||||
printf '[INFO] Temporary directory: %s\n' "$probe_dir"
|
||||
|
||||
# Каждый этап сохраняет собственный код завершения, включая молчаливые сбои.
|
||||
probe_step() {
|
||||
local step="$1" result
|
||||
shift
|
||||
printf '\n[INFO] %s\n[INFO] Directory: %s\n[INFO] Command:' "$step" "$PWD"
|
||||
printf ' %q' "$@"
|
||||
printf '\n'
|
||||
"$@"
|
||||
result=$?
|
||||
printf '[INFO] %s exit code: %d\n' "$step" "$result"
|
||||
if [ "$result" -ne 0 ]; then
|
||||
printf '[ERROR] Failed stage: %s\n' "$step"
|
||||
fi
|
||||
return "$result"
|
||||
}
|
||||
|
||||
printf '#include <stdio.h>\nint main(void) { puts("utun compiler probe OK"); return 0; }\n' > "$probe_dir/probe.c" || {
|
||||
printf '[ERROR] Cannot write the probe source: %s/probe.c\n' "$probe_dir"
|
||||
exit 1
|
||||
}
|
||||
probe_step 'Compiler version' "$probe_cc" --version || exit $?
|
||||
probe_step 'Compile' "$probe_cc" -v -std=c99 -c "$probe_dir/probe.c" -o "$probe_dir/probe.o" || exit $?
|
||||
probe_step 'Link' "$probe_cc" -v "$probe_dir/probe.o" -o "$probe_dir/probe.exe" || exit $?
|
||||
probe_step 'Run' "$probe_dir/probe.exe" || exit $?
|
||||
printf '\n[SUCCESS] Compile, link and run completed.\n'
|
||||
|
||||
probe_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) || {
|
||||
printf '[ERROR] Cannot locate the repository directory.\n'
|
||||
exit 1
|
||||
}
|
||||
probe_opus="$probe_root/lib/libopus"
|
||||
probe_includes=()
|
||||
for probe_include in '' include celt silk silk/float silk/fixed src; do
|
||||
probe_includes+=("-I$probe_opus/$probe_include")
|
||||
done
|
||||
for probe_source in laplace bands; do
|
||||
probe_step "Compile Opus $probe_source.c" "$probe_cc" -v -DOPUS_BUILD -DUSE_ALLOCA -DVAR_ARRAYS "${probe_includes[@]}" \
|
||||
-O3 -DNDEBUG -std=gnu99 -MD -MF "$probe_dir/$probe_source.d" \
|
||||
-c "$probe_opus/celt/$probe_source.c" -o "$probe_dir/$probe_source.o" || exit $?
|
||||
done
|
||||
|
||||
probe_build="$probe_root/tools/chatgui/build"
|
||||
if [ ! -f "$probe_build/CMakeCache.txt" ]; then
|
||||
printf '[ERROR] Existing ChatGUI build cache not found: %s/CMakeCache.txt\n' "$probe_build"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v cmake >/dev/null 2>&1; then
|
||||
printf '[ERROR] cmake is not available in PATH.\n'
|
||||
exit 1
|
||||
fi
|
||||
probe_step 'CMake cache settings' sed -n -E \
|
||||
'/^CMAKE_(C_COMPILER|C_COMPILER_LAUNCHER|C_FLAGS(_(DEBUG|RELEASE|MINSIZEREL|RELWITHDEBINFO))?|MAKE_PROGRAM|GENERATOR|BUILD_TYPE):/p' \
|
||||
"$probe_build/CMakeCache.txt" || exit $?
|
||||
|
||||
# Повторяем Windows-команду без преобразования аргументов MSYS2; добавляем только GCC -v.
|
||||
if [ "${MSYSTEM:-}" = UCRT64 ]; then
|
||||
probe_ninja=$(sed -n 's/^CMAKE_MAKE_PROGRAM:[^=]*=//p' "$probe_build/CMakeCache.txt")
|
||||
probe_ninja=${probe_ninja%$'\r'}
|
||||
probe_ninja=$(cygpath -u "$probe_ninja") || exit $?
|
||||
"$probe_ninja" -C "$probe_build" -t commands opus_internal > "$probe_dir/ninja-commands.txt"
|
||||
probe_result=$?
|
||||
if [ "$probe_result" -ne 0 ]; then
|
||||
cat "$probe_dir/ninja-commands.txt"
|
||||
printf '[ERROR] Cannot read Ninja commands, exit code: %d\n' "$probe_result"
|
||||
exit "$probe_result"
|
||||
fi
|
||||
probe_command=''
|
||||
while IFS= read -r probe_line; do
|
||||
probe_line=${probe_line%$'\r'}
|
||||
case "$probe_line" in
|
||||
*' -DOPUS_BUILD '*' -c '*'/celt/bands.c'|*' -DOPUS_BUILD '*' -c '*'/celt/bands.c"')
|
||||
probe_command="$probe_line"; break ;;
|
||||
esac
|
||||
done < "$probe_dir/ninja-commands.txt"
|
||||
if [ -z "$probe_command" ]; then
|
||||
printf '[ERROR] Cannot find the Opus bands.c compile command in Ninja.\n'
|
||||
exit 1
|
||||
fi
|
||||
probe_command=${probe_command/ -DOPUS_BUILD/ -v -DOPUS_BUILD}
|
||||
printf '[INFO] Native command: %s\n' "$probe_command"
|
||||
printf '@echo off\r\n%s\r\nexit /b %%errorlevel%%\r\n' "$probe_command" > "$probe_dir/replay.cmd" || {
|
||||
printf '[ERROR] Cannot write the native compiler replay script.\n'
|
||||
exit 1
|
||||
}
|
||||
probe_cmd=$(cygpath -u "${COMSPEC:-C:/Windows/System32/cmd.exe}") || exit $?
|
||||
probe_replay=$(cygpath -w "$probe_dir/replay.cmd") || exit $?
|
||||
|
||||
# CMD сохраняет полный Windows-код завершения cc1; Bash оставляет только младший байт.
|
||||
probe_cc1=$("$probe_cc" -print-prog-name=cc1) || { printf '[ERROR] Cannot locate cc1.\n'; exit 1; }
|
||||
probe_cc1=${probe_cc1%$'\r'}
|
||||
probe_cc1=$(cygpath -u "$probe_cc1") || { printf '[ERROR] Cannot resolve the cc1 path.\n'; exit 1; }
|
||||
probe_cc1_native=$(cygpath -w "$probe_cc1") || { printf '[ERROR] Cannot resolve the native cc1 path.\n'; exit 1; }
|
||||
printf '@echo off\r\n"%s" --version\r\nset "UTUN_CC1_RESULT=%%errorlevel%%"\r\n' "$probe_cc1_native" \
|
||||
> "$probe_dir/cc1.cmd" || { printf '[ERROR] Cannot write the cc1 probe script.\n'; exit 1; }
|
||||
printf 'echo [INFO] cc1 Windows exit code: %%UTUN_CC1_RESULT%%\r\nexit /b %%UTUN_CC1_RESULT%%\r\n' \
|
||||
>> "$probe_dir/cc1.cmd" || { printf '[ERROR] Cannot finish the cc1 probe script.\n'; exit 1; }
|
||||
probe_cc1_script=$(cygpath -w "$probe_dir/cc1.cmd") || { printf '[ERROR] Cannot resolve the cc1 script path.\n'; exit 1; }
|
||||
for probe_cwd in "$probe_root" "$probe_build"; do
|
||||
cd -- "$probe_cwd" || { printf '[ERROR] Cannot open probe directory: %s\n' "$probe_cwd"; exit 1; }
|
||||
MSYS2_ARG_CONV_EXCL='*' probe_step 'Launch cc1 before runtime refresh' "$probe_cmd" /d /c call "$probe_cc1_script"
|
||||
# При ошибке продолжаем сравнение каталогов и DLL, затем повторяем исходный сбой.
|
||||
done
|
||||
cd -- "$probe_root" || { printf '[ERROR] Cannot open the repository directory.\n'; exit 1; }
|
||||
probe_objdump="${probe_cc%/*}/objdump.exe"
|
||||
"$probe_objdump" -p "$probe_cc1" > "$probe_dir/cc1-imports.txt" || {
|
||||
printf '[ERROR] Cannot inspect cc1 imports.\n'; exit 1;
|
||||
}
|
||||
printf '\n[INFO] cc1 DLL imports:\n'
|
||||
sed -n '/DLL Name:/p' "$probe_dir/cc1-imports.txt" || { printf '[ERROR] Cannot read cc1 imports.\n'; exit 1; }
|
||||
source "$probe_root/tools/chatgui/compiler_runtime.sh"
|
||||
for probe_dll in "$probe_build/"*.dll; do
|
||||
[ -f "$probe_dll" ] || continue
|
||||
chatgui_is_compiler_runtime_dll "${probe_dll##*/}" || continue
|
||||
probe_system_dll="${probe_cc%/*}/${probe_dll##*/}"
|
||||
[ -f "$probe_system_dll" ] || continue
|
||||
cmp -s "$probe_dll" "$probe_system_dll"
|
||||
probe_result=$?
|
||||
case "$probe_result" in
|
||||
0) printf '[INFO] DLL matches UCRT64: %s\n' "${probe_dll##*/}" ;;
|
||||
1) printf '[WARN] DLL differs from UCRT64: %s\n' "${probe_dll##*/}"
|
||||
probe_step 'DLL hashes' sha256sum "$probe_dll" "$probe_system_dll" || exit $? ;;
|
||||
*) printf '[ERROR] Cannot compare DLL: %s, exit code: %d\n' "$probe_dll" "$probe_result"; exit "$probe_result" ;;
|
||||
esac
|
||||
done
|
||||
probe_step 'Refresh compiler runtime DLLs' chatgui_refresh_compiler_runtime "$probe_build" "${probe_cc%/*}" || exit $?
|
||||
cd -- "$probe_build" || { printf '[ERROR] Cannot open the ChatGUI build directory.\n'; exit 1; }
|
||||
MSYS2_ARG_CONV_EXCL='*' probe_step 'Launch cc1 after runtime refresh' "$probe_cmd" /d /c call "$probe_cc1_script" || exit $?
|
||||
MSYS2_ARG_CONV_EXCL='*' probe_step 'Replay Ninja bands.c command with GCC verbose output' \
|
||||
"$probe_cmd" /d /c call "$probe_replay" || exit $?
|
||||
fi
|
||||
probe_step 'Build Opus with CMake' cmake --build "$probe_build" --target opus_internal --parallel 1 --verbose || exit $?
|
||||
printf '\n[SUCCESS] Standalone Opus compilation and CMake Opus build completed.\n'
|
||||
+63
-5
@@ -62,11 +62,24 @@ AC_ARG_ENABLE([hardening],
|
||||
[enable_hardening=no])
|
||||
|
||||
if test "x$enable_hardening" = "xyes"; then
|
||||
HARDEN_CFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fstack-clash-protection -Wformat=2 -Wformat-overflow=2 -fPIE"
|
||||
HARDEN_LDFLAGS="-Wl,-z,relro -Wl,-z,now -pie"
|
||||
HARDEN_CFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -Wformat=2"
|
||||
compiler_version=$($CC --version 2>&1) || AC_MSG_ERROR([Cannot identify compiler for hardening])
|
||||
case "$compiler_version" in
|
||||
*clang*) ;;
|
||||
*) HARDEN_CFLAGS="$HARDEN_CFLAGS -fstack-clash-protection -Wformat-overflow=2" ;;
|
||||
esac
|
||||
case "$host" in
|
||||
*mingw* | *msys* | *cygwin*)
|
||||
HARDEN_LDFLAGS="-Wl,--dynamicbase,--nxcompat,--high-entropy-va"
|
||||
;;
|
||||
*)
|
||||
HARDEN_CFLAGS="$HARDEN_CFLAGS -fPIE"
|
||||
HARDEN_LDFLAGS="-Wl,-z,relro -Wl,-z,now -pie"
|
||||
;;
|
||||
esac
|
||||
CFLAGS="$CFLAGS $HARDEN_CFLAGS"
|
||||
LDFLAGS="$LDFLAGS $HARDEN_LDFLAGS"
|
||||
AC_MSG_NOTICE([Hardening flags enabled: $HARDEN_CFLAGS])
|
||||
AC_MSG_NOTICE([Hardening flags enabled: CFLAGS=$HARDEN_CFLAGS LDFLAGS=$HARDEN_LDFLAGS])
|
||||
fi
|
||||
|
||||
# ==================== НАДЁЖНЫЙ ДЕТЕКТ WINDOWS ====================
|
||||
@@ -95,6 +108,19 @@ AC_MSG_RESULT([$os_freebsd])
|
||||
|
||||
AM_CONDITIONAL([OS_FREEBSD], [test "x$os_freebsd" = "xyes"])
|
||||
|
||||
# ==================== DETECT LINUX ====================
|
||||
AC_MSG_CHECKING([for Linux])
|
||||
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
|
||||
#ifndef __linux__
|
||||
#error "Not Linux"
|
||||
#endif
|
||||
]], [[ return 0; ]])],
|
||||
[os_linux=yes],
|
||||
[os_linux=no])
|
||||
AC_MSG_RESULT([$os_linux])
|
||||
|
||||
AM_CONDITIONAL([OS_LINUX], [test "x$os_linux" = "xyes"])
|
||||
|
||||
# Set TUN libs based on platform
|
||||
if test "x$os_windows" = "xyes"; then
|
||||
TUN_LIBS="-liphlpapi -lws2_32 -ladvapi32 -lbcrypt -ldbghelp"
|
||||
@@ -165,6 +191,37 @@ AM_CONDITIONAL([HAVE_FFMPEG], [test "x$with_ffmpeg" = "xyes"])
|
||||
AC_SUBST([FFMPEG_LIBS])
|
||||
AC_SUBST([FFMPEG_CFLAGS])
|
||||
|
||||
# ==================== SILERO VAD (OPTIONAL, requires ONNX Runtime) ====================
|
||||
AC_ARG_WITH([silero-vad],
|
||||
AS_HELP_STRING([--with-silero-vad], [Enable Silero voice activity detector (requires ONNX Runtime)]),
|
||||
[with_silero_vad=$withval],
|
||||
[with_silero_vad=no])
|
||||
|
||||
if test "x$with_silero_vad" = "xyes"; then
|
||||
AC_CHECK_HEADER([onnxruntime/onnxruntime_c_api.h], [
|
||||
save_LIBS="$LIBS"
|
||||
LIBS="-lonnxruntime $LIBS"
|
||||
AC_CHECK_LIB([onnxruntime], [OrtGetApiBase], [
|
||||
SILERO_VAD_LIBS="-lonnxruntime"
|
||||
AC_DEFINE([HAVE_SILERO_VAD], [1], [Define to enable Silero VAD via ONNX Runtime])
|
||||
AC_MSG_NOTICE([Silero VAD enabled (ONNX Runtime)])
|
||||
], [
|
||||
AC_MSG_WARN([libonnxruntime not found, disabling silero-vad])
|
||||
with_silero_vad=no
|
||||
])
|
||||
LIBS="$save_LIBS"
|
||||
], [
|
||||
AC_MSG_WARN([onnxruntime/onnxruntime_c_api.h not found, disabling silero-vad])
|
||||
with_silero_vad=no
|
||||
])
|
||||
fi
|
||||
|
||||
if test "x$with_silero_vad" != "xyes"; then
|
||||
SILERO_VAD_LIBS=""
|
||||
fi
|
||||
AM_CONDITIONAL([HAVE_SILERO_VAD], [test "x$with_silero_vad" = "xyes"])
|
||||
AC_SUBST([SILERO_VAD_LIBS])
|
||||
|
||||
# Checks for header files, typedefs, functions...
|
||||
AC_CHECK_HEADERS([arpa/inet.h fcntl.h limits.h netinet/in.h stdint.h stdlib.h string.h sys/ioctl.h sys/socket.h unistd.h])
|
||||
AC_TYPE_SIZE_T
|
||||
@@ -191,10 +248,11 @@ AC_OUTPUT
|
||||
# Summary
|
||||
echo "
|
||||
Configuration summary:
|
||||
----------------------
|
||||
---------------------
|
||||
Host: ${host}
|
||||
Windows: ${os_windows}
|
||||
Prefix: ${prefix}
|
||||
Compiler: ${CC}
|
||||
CFLAGS: ${CFLAGS}
|
||||
"
|
||||
Silero VAD: ${with_silero_vad}
|
||||
"
|
||||
+3
-3
@@ -197,14 +197,14 @@ SRC_SOURCES=(
|
||||
routing_layer/route_lib.c routing_layer/route6_lib.c topo_bgp.c routing_layer/route_ping.c
|
||||
routing_layer/topo_node.c topo_node_lmdb.c routing_layer/route_connectivity.c routing_layer/conn_mgr.c
|
||||
routing_layer/routing.c tun_if.c tun_route.c tun_linux.c tun_freebsd.c tun_windows.c
|
||||
transport_layer/etcp.c transport_layer/etcp_connections.c transport_layer/etcp_bbr.c transport_layer/etcp_loadbalancer.c
|
||||
transport_layer/etcp.c transport_layer/etcp_session.c transport_layer/etcp_connections.c transport_layer/etcp_bbr.c transport_layer/etcp_loadbalancer.c
|
||||
transport_layer/etcp_debug.c transport_layer/etcp_dump.c transport_layer/secure_channel.c transport_layer/crc32.c
|
||||
transport_layer/stcp_link.c transport_layer/stcp.c transport_layer/stcp_server.c transport_layer/stcp_client.c
|
||||
transport_layer/pkt_normalizer.c transport_layer/packet_dump.c transport_layer/etcp_api.c
|
||||
control_server.c
|
||||
firewall.c eim_nat.c nat_transport.c transport_layer/dummynet.c
|
||||
etcp_router.c route_crypto.c
|
||||
proxy/udp_proxy.c proxy/socks_proxy.c
|
||||
proxy/udp_proxy.c proxy/socks_proxy.c proxy/http_proxy.c
|
||||
lwip_tcp/lwip_pbuf.c
|
||||
)
|
||||
|
||||
@@ -272,7 +272,7 @@ echo ""
|
||||
if [ -f "$SCRIPT_DIR/lib/wintun.dll" ]; then
|
||||
echo "Run on Windows:"
|
||||
echo " 1. Install Wintun driver (https://www.wintun.net)"
|
||||
echo " 2. Copy utun.exe + wintun.dll to the same directory"
|
||||
echo " 2. Copy utun.exe + wintun.dll + LICENSE + LICENSING + THIRD_PARTY_NOTICES to the same directory"
|
||||
echo " 3. Run: utun.exe -f -p utun.pid -l utun.log"
|
||||
else
|
||||
echo "Run on Windows:"
|
||||
|
||||
+1524
File diff suppressed because it is too large.
Load diff
+335
-69
@@ -1,77 +1,160 @@
|
||||
# P2P аудио-звонок пользователю — архитектура
|
||||
# P2P аудио- и видеозвонок пользователю — архитектура
|
||||
|
||||
Статус: спроектировано, ждёт реализации.
|
||||
Статус: реализовано. Звонок использует роутер, CM подготавливает оптимальный путь.
|
||||
|
||||
## 1. Область
|
||||
|
||||
Голос 1:1. Звонок можно инициировать **из CHAT-группы или из p2p-чата (DM)** — модуль
|
||||
не привязан к типу группы: он оперирует `group_id` + `peer_node_id` (для DM `group_id` —
|
||||
DM-группа, маршрутизация через `etcp_router` идентична). Протокол расширяем под видео
|
||||
(track-модель), реализуется только аудио. Работает desktop↔desktop и desktop↔android
|
||||
Аудио и видео 1:1. Звонок можно инициировать **из CHAT-группы или из p2p-чата (DM)**.
|
||||
Модуль оперирует `group_id` + `peer_node_id`. Для `dm:<conv_id>` функция `dm_call_route`
|
||||
выбирает общую CHAT-группу с живым BGP-маршрутом к собеседнику; отдельная DM-группа не создаётся.
|
||||
Аудио и видео используют отдельные каналы `etcp_router`; возможности видеокодеков
|
||||
согласуются в активном звонке (см. [call_video.txt](call_video.txt)). Работает desktop↔desktop и desktop↔android
|
||||
(обе стороны компилируют общий стек `etcp_router` / `topo_group` (BGP) / `conn_mgr`).
|
||||
|
||||
Полный цикл: проверка онлайна (BGP) → INVITE → звонок → ACCEPT/DECLINE →
|
||||
установка соединения (conn_mgr) → аудиопоток → HANGUP.
|
||||
аудиопоток и опциональное видео через роутер → HANGUP. CM инициируется вызывающим при старте, вызываемым при принятии вызова.
|
||||
|
||||
### Входящие вызовы и телефонная занятость
|
||||
|
||||
Android хранит пять независимых настроек: показывать статус звонка, предупреждать вызывающего,
|
||||
отклонять при телефонной занятости, отклонять все обычные звонки, отклонять все приглашения в рацию.
|
||||
Они передаются маской `CALL_POLICY_*` через `[chat] incoming_call_policy` при старте,
|
||||
а при изменениях — вместе с живым состоянием телефона через JNI в uasync.
|
||||
Первые три настройки используют `READ_PHONE_STATE`; запреты всех входящих разрешения не требуют.
|
||||
Наблюдатель учитывает все активные SIM, набор номера, удержание и входящий телефонный вызов.
|
||||
Без разрешения или без телефонной службы состояние неизвестно, а не свободно.
|
||||
|
||||
Приоритет: запрет типа вызова → автоматический отказ при телефонной занятости → предупреждение с выбором.
|
||||
Отказ при занятости возвращает `CALL_REASON_PHONE_BUSY`/`RADIO_CALL_PHONE_BUSY`.
|
||||
Предупреждение переводит попытку в `WAIT_DECISION`: у вызываемого нет события INCOMING, рингтона и регистрации в Telecom.
|
||||
Вызывающий видит «Абонент разговаривает по телефону», «Всё равно позвонить» и «Завершить».
|
||||
Продолжение подтверждается повторяемой командой внутри той же попытки; освобождение телефона само не запускает звонок.
|
||||
Повторные пакеты не создают повторных входящих событий. При изменении политики перед продолжением запрет проверяется снова.
|
||||
Предел всей попытки — 60с от начала, включая выбор; продолжение deadline не меняет.
|
||||
У рации первый ответ и подтверждение продолжения ограничены 2с; сообщение о занятости является ответом, но не RINGING.
|
||||
|
||||
Рация, уже включённая у адресата в указанном канале, возвращает `RADIO_CALL_ALREADY_ACTIVE`:
|
||||
вызывающий получает уведомление, входящий вызов не показывается, его собственная рация не включается.
|
||||
Этот ответ описывает существующую подписку и не создаёт нового входящего вызова даже при включённом запрете приглашений.
|
||||
Принятие обычного приглашения в рацию по-прежнему сначала включает местную подписку, затем отправляет ACCEPT.
|
||||
Android обычного звонка показывает своё уведомление после `onShowIncomingCallUi` и обрабатывает отказ Telecom.
|
||||
|
||||
## 2. Разбиение файлов (отдельный каталог `src/call/`)
|
||||
|
||||
| Файл | Язык | Сборки | Назначение |
|
||||
|---|---|---|---|
|
||||
| `src/call/call_proto.h` | C | везде | wire-формат, subcmd, svc_id `ETCP_RT_ID_CALL 0x34` |
|
||||
| `src/call/call_proto.h` | C | везде | wire-формат сигналинга (`0x36`) и аудио (`0x3F`), subcmd |
|
||||
| `src/call/call.h` / `call.c` | C | везде (autotools + GUI) | сигналинг, реестр сессий, таймеры, watchdog (20с по приёму), релей медиа с backpressure, счётчики |
|
||||
| `src/call/call_audio.h/c` | C | только GUI-сборки | Opus encode/decode, фрейминг, pull-обвязка с тонами; регистрация через `call_audio_ops` |
|
||||
| `src/call/call_tones.h/c` | C | только GUI-сборки | генератор тонов (glitch/ended), синус |
|
||||
| `src/call/audio_jitter.h/cpp` | C++ | только GUI-сборки | SoundTouch джиттер-буфер, C API |
|
||||
| `src/call/call_jitter.h/cpp` | C++ | только GUI-сборки | SoundTouch джиттер-буфер, C API |
|
||||
| `src/call/call_video.h/c`, `call_video_transport.h/c` | C | везде | формат видеотрека, согласование возможностей и передача кадров отдельными каналами |
|
||||
|
||||
Autotools-сборка демона остаётся C-only (`.cpp` не компилируется); сигналинг `call.c`
|
||||
присутствует и в демоне, аудиопроцессинг — только там, где есть GUI.
|
||||
|
||||
Новая категория логирования: `DEBUG_CATEGORY_CALL = 31` (добавить в `debug_config.h`,
|
||||
`DEBUG_CATEGORY_COUNT` → 32).
|
||||
Диагностика: категория `call` (`DEBUG_CATEGORY_CALL`); транспортная доставка — `etcp_route`.
|
||||
Desktop направляет сообщения FFmpeg и Qt в общий `debug_file` (по умолчанию `chatgui.log`).
|
||||
Начальная проверка codec пишет краткий итог на info, детали и ожидаемые отказы — на debug:
|
||||
`[gui] debug_categories=call=debug,video=debug`. Повторы FFmpeg внутри пробы сворачиваются со счётчиком;
|
||||
рабочие ошибки параллельного звонка сохраняют error/warn. Информационный stderr libva отключён по умолчанию;
|
||||
для диагностики драйвера можно явно запустить с `LIBVA_MESSAGING_LEVEL=2`.
|
||||
Отсутствующая desktop-запись приложения создаётся в XDG data applications для регистрации в portal.
|
||||
|
||||
## 3. Протокол (svc_id 0x34, первый байт payload = subcmd)
|
||||
|
||||
Формат приёма (как media_delivery, одноуровневый): `entry->dgram[0]=svc_id`,
|
||||
`entry->dgram[1]=subcmd`, `entry->dgram[2..]=payload`.
|
||||
## 3. Протокол
|
||||
|
||||
Каналы роутера независимы:
|
||||
|
||||
| svc_id | Константа | Назначение |
|
||||
|---|---|---|
|
||||
| `0x36` | `ETCP_RT_ID_CALL` | сигналинг звонка |
|
||||
| `0x3D` | `ETCP_RT_ID_CALL_VIDEO_CONTROL` | согласование и восстановление видеотрека |
|
||||
| `0x3E` | `ETCP_RT_ID_CALL_VIDEO` | видеокадры |
|
||||
| `0x3F` | `ETCP_RT_ID_CALL_AUDIO` | аудиокадры `CALL_MEDIA` |
|
||||
|
||||
Формат приёма сигналинга и аудио: `entry->dgram[0]=svc_id`, служебный заголовок роутера до
|
||||
`ROUTER_SVC_PAYLOAD_OFF=26`, затем subcmd и тело команды.
|
||||
|
||||
Сигналинг (`0x36`):
|
||||
|
||||
```
|
||||
CALL_INVITE 0x01 {call_id:8, group_id:8, tracks:[{type:AUDIO, codec:OPUS, sr:4, ch:1, bitrate:4}]}
|
||||
CALL_INVITE 0x01 {call_id:8, group_id:8, num_tracks:1, force:1, expires_us:8, tracks:[{type:AUDIO, codec:OPUS, sr:4, ch:1, bitrate:4}]}
|
||||
CALL_RINGING 0x02 {call_id:8}
|
||||
CALL_ACCEPT 0x03 {call_id:8, agreed_tracks}
|
||||
CALL_ACCEPT 0x03 {call_id:8}
|
||||
CALL_DECLINE 0x04 {call_id:8, reason:1}
|
||||
CALL_BUSY 0x05 {call_id:8}
|
||||
CALL_CANCEL 0x06 {call_id:8}
|
||||
CALL_HANGUP 0x07 {call_id:8, reason:1}
|
||||
CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, payload}
|
||||
CALL_END_ACK 0x08 {call_id:8}
|
||||
CALL_PHONE_BUSY 0x09 {call_id:8}
|
||||
```
|
||||
|
||||
Аудио (`0x3F`):
|
||||
|
||||
```
|
||||
CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, capture_pts_us:8, capture_epoch:8, codec_delay_samples:4, payload}
|
||||
```
|
||||
|
||||
- `call_id` — случайный uint64, генерит вызывающий; идентифицирует сессию на обеих сторонах.
|
||||
- `group_id` — CHAT-группа или DM-группа (общая маршрутизация).
|
||||
- `group_id` — CHAT-группа; для DM-звонка выбирается общая группа участников с живым BGP-маршрутом.
|
||||
- Сигналинг: `etcp_route_send(..., ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)`.
|
||||
- Медиа: без подписи (уже внутри шифрованного канала etcp_router).
|
||||
- `CALL_MEDIA` — общий контейнер; `track_id` оставляет место видео (`type=VIDEO`).
|
||||
- Сигналинг, аудио и видео: обязательные `ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT` между конечными узлами.
|
||||
- AES-128-CCM шифрует тело, Ed25519 подписывает заголовок роутера и шифртекст. Посредники не имеют ключа для расшифровки.
|
||||
- Ключ получается из постоянных X25519-идентичностей собеседников; forward secrecy отсутствует.
|
||||
- Маска защиты задаётся через `etcp_router_require_crypto`: открытые/частично защищённые данные отбрасываются
|
||||
на конечном узле до изменения состояния приёма, очередей и `receive_ready`; отправка без требуемых флагов возвращает ошибку.
|
||||
Пустые служебные пакеты роутера не являются медиаданными. Перехода на открытое медиа при ошибках нет.
|
||||
- Каждый пакет получает случайный nonce(13) от OpenSSL CSPRNG; ошибка RNG прекращает шифрование.
|
||||
Надбавка к сервисным данным: nonce(13) + CRC(4) + CCM tag(16) + Ed25519(64) = 97 байт.
|
||||
- `CALL_MEDIA` переносит только аудио (`track_id=CALL_TRACK_AUDIO`); остальные значения приёмник отклоняет.
|
||||
Видео имеет отдельный формат и транспорт: `call_video.h`, `call_video_transport.h`, [call_video.txt](call_video.txt).
|
||||
- Аудио: Opus 48kHz mono, 20ms (960 сэмплов), ~24–32 kbps. ~50 кадров/с.
|
||||
- Заголовок медиа — 36 байт с subcmd. `ts_ms` — время отправки для оценки сетевого джиттера;
|
||||
`capture_pts_us` — время первого входного сэмпла в монотонной шкале отправителя, `INT64_MIN` при отсутствии метки.
|
||||
`capture_epoch` различает разрывы захвата; `codec_delay_samples` передаёт Opus lookahead.
|
||||
Старый формат не поддерживается; медиа принимается только от пира текущего звонка.
|
||||
|
||||
## 4. Машина состояний
|
||||
|
||||
```
|
||||
Caller: IDLE → OUTGOING(INVITE) → RINGING(RINGING) → CONNECTING(ACCEPT, conn_mgr_open) → ACTIVE → ENDED
|
||||
Callee: IDLE → INCOMING(INVITE) → RINGING(sent RINGING) → ACCEPTING(accept, conn_mgr_open) → ACTIVE → ENDED
|
||||
Caller: IDLE → OUTGOING(INVITE) → RINGING(RINGING) → ACTIVE(ACCEPT) → ENDED
|
||||
Callee: IDLE → INCOMING(INVITE) → RINGING(sent RINGING) → ACTIVE(accept) → ENDED
|
||||
```
|
||||
|
||||
`ENDED` reasons: `LOCAL_HANGUP / REMOTE_HANGUP / DECLINE / BUSY / CANCEL /
|
||||
NO_TRAFFIC / RING_TIMEOUT / CONNECT_FAIL`.
|
||||
|
||||
Таймауты:
|
||||
- ring-timeout 45с (нет ответа → CANCEL);
|
||||
- connect: `conn_mgr` `CONN_EVENT_TIMEOUT` → fail;
|
||||
- **кодограмма INVITE 300мс** — вызывающий шлёт её весь период вызова
|
||||
(`OUTGOING` + `RINGING`) как «сердцебиение»: покрывает потерю INVITE/RINGING и
|
||||
держит маршрут живым;
|
||||
- **watchdog 2с у вызываемого** — нет кодограммы 2с → вызов считается сорванным
|
||||
(отмена потерялась), сессия закрывается локально (`CALL_REASON_CANCEL`);
|
||||
- ring-timeout 60с от начала, включая ожидание решения вызывающего (нет ответа → CANCEL);
|
||||
- `CONN_EVENT_DOWN/TIMEOUT` CM не завершают звонок: роутер использует доступные пути;
|
||||
- **NO_TRAFFIC 20с** — нет входящих медиа-кадров → авто HANGUP.
|
||||
|
||||
Завершающие кодограммы HANGUP/CANCEL/DECLINE повторяются каждые 300мс до END_ACK,
|
||||
не дольше 20с. Это необходимо при смене эпохи роутера: старые очереди удаляются,
|
||||
даже если отправитель ранее получил успешный результат постановки команды в очередь.
|
||||
Медиа и их локальные очереди освобождаются сразу, ENDED публикуется один раз.
|
||||
В реестре до ACK/таймаута остаётся только завершённая сессия с CM handle и таймером.
|
||||
Получатель подтверждает также повторные команды после удаления сессии, без повторного ENDED.
|
||||
Команда и ACK проверяются по подписи, шифрованию, размеру, call_id, пиру и группе существующей сессии.
|
||||
ACK не подтверждается. Новый звонок имеет другой call_id и не затрагивается поздними командами старого.
|
||||
При одновременном завершении стороны независимо подтверждают команды друг друга.
|
||||
После ACK сессия удаляется на ближайшем tick завершения; при удалённом завершении CM удерживается
|
||||
ещё 500мс для отправки ACK. Остановка ядра отменяет таймер без повторных отправок.
|
||||
Запоздалые кодограммы вызова при идущем разговоре (`ACTIVE`) игнорируются.
|
||||
|
||||
Онлайн-проверка: `topo_node_find_by_id(group, peer) == NULL` → «не в сети» (event).
|
||||
|
||||
Соединение: на ACCEPT обе стороны `conn_mgr_open(inst, group_id, peer, cb, ...)`;
|
||||
`CONN_EVENT_UP` → ACTIVE. Медиа идёт `etcp_route_send` (после подъёма прямого линка
|
||||
роутер идёт напрямую — RTT минимален).
|
||||
CM handle инициирует оптимальное подключение и удерживается до завершения звонка.
|
||||
Медиа всегда идёт через `etcp_route_send`: роутер сам выбирает лучший путь и fallback.
|
||||
Звонок не хранит ETCP_CONN и не завершается при потере отдельного транспорта.
|
||||
Очистка записи CM отсоединяет handles и отменяет отложенные UP; handle закрывает владелец.
|
||||
Закрытие CM handle не посылает DISCONNECT удалённым владельцам. Механизм NCD не меняется.
|
||||
|
||||
## 5. Отправка медиа — backpressure через локальный буфер
|
||||
|
||||
@@ -87,49 +170,213 @@ NO_TRAFFIC / RING_TIMEOUT / CONNECT_FAIL`.
|
||||
|
||||
В сессии:
|
||||
- `tx_q` — локальная FIFO очередь закодированных кадров (ll_entry с dgram), ёмкостью
|
||||
~1с (≈50 кадров). При переполнении — дроп старейшего + счётчик `c_tx_dropped`.
|
||||
15с (750 кадров). При переполнении — дроп старейшего + счётчик `c_tx_dropped`.
|
||||
- Алгоритм (`call_tx_flush`): пока `tx_q` не пуст:
|
||||
- если `send_q_has_room` → `queue_data_get` + `etcp_route_send(force=0)`;
|
||||
- если `send_q_has_room` → `queue_data_get` + `etcp_route_send(force=1)` (после проверки места);
|
||||
- иначе — если waiter не зарегистрирован → `etcp_router_on_send_ready(... drain_cb)` и выход.
|
||||
- `drain_cb(q, arg)` → снова `call_tx_flush(session)` (re-arm при необходимости).
|
||||
- Весь код — в uasync-потоке (single-threaded, реентерабельность как в
|
||||
`media_delivery.c:stream_send_chunk_cb`).
|
||||
|
||||
## 6. Thread-модель (single-writer)
|
||||
## 6. Потоки и жизненный цикл
|
||||
|
||||
| Объект | Владелец |
|
||||
|---|---|
|
||||
| `call_session` (состояние, encoder, decoder, tx_q, таймеры) | uasync-поток |
|
||||
| Opus encoder (send) | аудио-поток (encode) |
|
||||
| Opus decoder (recv) | uasync-поток (`decode → ajb_push`) |
|
||||
| `ajb` push | uasync-поток |
|
||||
| `ajb` pull + SoundTouch | аудио-поток |
|
||||
| `call_tones` | аудио-поток |
|
||||
`call_session`, CM handle, очередь TX и таймеры принадлежат uasync-потоку.
|
||||
Аудиопоток кодирует PCM и постит `call_send_media`; приём в uasync кладёт Opus в `call_jitter`.
|
||||
Декодер, SoundTouch и генератор тонов принадлежат аудиопотоку.
|
||||
Кольцо кодированных кадров и снимок статистики защищены мьютексом; статистика не читает SoundTouch из другого потока.
|
||||
`CallTimeline` хранит фактическое соответствие входных участков SoundTouch выходному PCM.
|
||||
Платформа публикует позицию воспроизведения с локальной монотонной меткой; `call_audio_get_clock`
|
||||
возвращает исходный PTS слышимого участка. Локальная тишина и тоны удерживают PTS.
|
||||
Невалидная/устаревшая отметка устройства не разрешает продвижение видео. Контракт и ограничения — `call_video.txt`.
|
||||
|
||||
Поток данных:
|
||||
- TX: аудио-поток кодирует → `call_send_encoded(call_id, opus, len)` → `uasync_post` →
|
||||
uasync: `tx_q` + `call_tx_flush` (backpressure).
|
||||
- RX: uasync принимает `CALL_MEDIA` → decode → `ajb_push`; аудио-поток `call_pull_pcm` → `ajb_pull` (+ тоны).
|
||||
GUI запускает аудиоустройство по CALL_ACCEPTED. При CALL_ENDED начинается финальный тон;
|
||||
устройство останавливается перед `call_audio_release`. Явное завершение звонка прекращает
|
||||
воспроизведение накопленной речи и включает тон завершения — на обеих платформах.
|
||||
Android останавливает гудки до настройки звонкового аудиотракта. После выбора маршрута
|
||||
capture/play потоки независимо создают и запускают AudioRecord/AudioTrack; каждый сразу обслуживает своё направление.
|
||||
`start()` подтверждает запуск подготовки, ошибки устройств передаются владельцу на main через `onFailure`.
|
||||
Ошибка любого направления завершает звонок. Общий `stop()` отменяет подготовку, разблокирует I/O и ждёт оба потока
|
||||
до освобождения Android-устройств и native-аудиостека. Отдельные locks защищают подготовку/остановку каждого устройства.
|
||||
Замер AEC планируется через 300мс после готовности обоих направлений и не задерживает воспроизведение.
|
||||
При наличии Telecom Connection режимом и выходом звонка управляет только Telecom.
|
||||
ACTIVE запрашивается до создания аудиоустройств; AudioManager/прямой preferred device не дублируют выбор.
|
||||
На API 34+ маршрут запрашивается через доступный CallEndpoint, на старых — через Connection.setAudioRoute.
|
||||
Подтверждённый маршрут приходит из callback системы, отдельно от желания пользователя; ошибка запроса логируется.
|
||||
Если Connection ещё нет, используется AudioManager. Поздняя регистрация передаёт управление Telecom,
|
||||
снимая заявку приложения на режим и прямые устройства. Рация сохраняет отдельный AudioManager-путь.
|
||||
Реальное время запуска/переключения зависит от Android и требует проверки на устройстве.
|
||||
Маршрутизатор использует один снимок входных/выходных устройств; обновляет его при старте,
|
||||
событиях устройств/BT-профиля и явной переоценке. По завершении снимок освобождается.
|
||||
INFO-логи Android показывают длительности настройки режима, регистрации callback, BT-профиля, выбора маршрута,
|
||||
создания/старта устройств, первого чтения микрофона и первой записи в AudioTrack, а также готовность другого направления.
|
||||
Общий `call_audio` логирует время ожидания первого PCM; запись в AudioTrack может содержать начальную тишину.
|
||||
Эффективный рабочий буфер AudioTrack на всех поддерживаемых API при старте запрашивается размером
|
||||
два блока PCM (40мс); на API 31+ стартовый порог следует фактически принятому размеру.
|
||||
Ёмкость выделенной памяти может быть больше. Ограничение повторяется по callback фактической
|
||||
смены выхода на main, под trackLock; callback старого AudioTrack игнорируется и снимается при stop.
|
||||
Подачу PCM задаёт свободное место в AudioTrack, без дополнительного таймера 20мс.
|
||||
Частичная запись удерживает остаток одного блока до полной передачи; следующий pull ждёт.
|
||||
Сетевой запас остаётся в адаптивном JB, его рост и SoundTouch-догон не изменены.
|
||||
Лог `playback buffering` показывает причину настройки, запрошенный/принятый размер, порог и ёмкость;
|
||||
ежесекундный `audio summary` — queueUs, текущий порог и underruns; новый episode underrun имеет уровень WARN.
|
||||
Это очередь AudioTrack, а не полная сквозная задержка и не сетевой jitter reserve.
|
||||
Ранее на Samsung startFrames=1920 был принят, но без ограничения рабочего буфера
|
||||
written-played оставался 120–160мс; одного стартового порога недостаточно.
|
||||
Замер Samsung подтвердил постоянную очередь 40мс. При прежнем параллельном управлении маршрутом
|
||||
AudioTrack пересоздавался, очередь временно достигала 160мс, а ожидание свободного места — 328–336мс.
|
||||
При старте AudioTrack.play занимал 1080мс. После перехода к единственному владельцу Telecom
|
||||
в проверенном звонке старт занял 62мс, очередь оставалась не более 40мс и при смене выхода.
|
||||
Переключение всё ещё останавливало потребление на 293–328мс; этот всплеск принимает сетевой JB.
|
||||
|
||||
`ajb` = mutex-кольцо PCM (гранулярность 20ms/50 ops в сек — мьютекс бесплатен);
|
||||
SoundTouch инстанс заперт на pull-стороне.
|
||||
На desktop `AudioDiag: capture clock` при call=debug выводит секундную сводку исходных backend-блоков:
|
||||
число блоков/сэмплов, накопленную последовательность, диапазон ошибки относительно конца предыдущего блока,
|
||||
latency PulseAudio, число скачков >2мс, hole-сэмплы и блоки без достоверного измерения.
|
||||
Первый скачок за секунду содержит обе latency/поправки конвертера, expected/actual PTS и монотонное время.
|
||||
Backend публикует только числовой снимок в ограниченное кольцо; форматирование выполняет reporter.
|
||||
Исходный backend-снимок остаётся наблюдением. В capture worker позиция PCM задаёт непрерывные PTS,
|
||||
а backend медленно корректирует их привязку: постоянная времени 2с, предел 0.5% (100мкс на 20мс PCM).
|
||||
Скорректированное время используется и для Opus, и для выбора AEC-референса. Сам PCM не изменяется.
|
||||
Разрыв sequence, смена route и скачок исходных меток >20мс по-прежнему сбрасывают capture/AEC и эпоху;
|
||||
порог 2мс в call_audio не увеличивается. Секундный `voice-io: capture clock` показывает phase/error/adjustment.
|
||||
Тесты проверяют десять минут дрейфа +/-300ppm, скачки оценки до 14мс, разбиение чанков 25мс на Opus 20мс,
|
||||
сохранность PCM, явную потерю, смену устройства и большой разрыв исходных часов.
|
||||
Счётчик sequence описывает полученные backend-сэмплы; отсутствие потерь очереди само по себе
|
||||
не доказывает непрерывность физического захвата. Проверять вместе с hole/overflow и route-событиями.
|
||||
|
||||
**Контракт жизненного цикла:** GUI стартует аудио-устройство по `CALL_ACCEPTED`,
|
||||
останавливает по `CALL_ENDED` и только после остановки (join аудио-потока) вызывает
|
||||
`call_release(call_id)` (post в uasync) → uasync освобождает session/encoder/decoder/jb.
|
||||
Пока устройство работает — `call_pull_pcm`/`call_send_encoded` не гоняются с освобождением.
|
||||
Для захвата звонка desktop явно запрашивает период 20мс вместо дефолта PulseAudio 25мс;
|
||||
фактический период сообщает `audio: call-capture initialized`. Сборщик по-прежнему принимает произвольные чанки.
|
||||
Секундные замеры задержки при `call=debug`:
|
||||
- `voice-io: capture latency`: возраст первого сэмпла перед native feed (включает сам блок 20мс),
|
||||
подготовка/AEC в worker и вызов native feed; диапазоны min/avg/max в микросекундах.
|
||||
- `voice-io: playback latency`: приблизительный остаток PCM после mix, включая частичный блок,
|
||||
длительность pull и недостающие сэмплы. Это очередь приложения, отдельно от backend.
|
||||
- `AudioDiag: playback clock`: latency backend, задержка конвертера/cache и максимальный размер
|
||||
backend-порции в микросекундах. При measured=0 нулевые границы не означают нулевую задержку.
|
||||
- Windows `AudioDiag: WASAPI queue`: текущий padding shared-mode, отдельный stream latency,
|
||||
converter/cache, размер callback и незавершённого mapped buffer. Диапазоны — min/avg/max в мкс;
|
||||
`padding_samples=0` означает отсутствие пригодного замера, а не пустую очередь.
|
||||
Снимок снимается перед заполнением client PCM; запись и форматирование выполняет reporter.
|
||||
`AudioDiag: audio timing` для стадий `callback`/`write` при `call=debug` показывает интервалы
|
||||
callback и время блокирующей передачи в WASAPI. HRESULT ошибок и потери диагностической очереди логируются.
|
||||
Снимки только наблюдают: Windows-аудиочасы остаются `callback-estimate` (`measured=0`).
|
||||
- `video playout`: показанные кадры, `speed_ppm`, диапазон A/V-фазы, интервалы показа,
|
||||
опоздание worker и ожидания аудиочасов. Опоздание кадра не вызывает пропуск; догон до 1.6x.
|
||||
- Desktop `video RX worker`: число декодированных кадров, максимальное время декодирования,
|
||||
возраст кадра после декодирования и опоздание пробуждения worker после sleep. Снимок аудио
|
||||
содержит `audio_measured`, PTS и позиции played/written. Все эти строки относятся к `call=debug`;
|
||||
`video=trace` для этой диагностики не требуется.
|
||||
- `video GUI delivery`: доставленные изображения, ожидание уведомления, промежуток доставки,
|
||||
`paint_wait_max_us` до подтверждения рисования. В GUI один ожидающий кадр, замены нет.
|
||||
- `video port TX paused/resumed`: сохранённый хвост пачки, первый ID, длительность ожидания
|
||||
и число повторов. Busy сохраняет encoded-кадры и не запрашивает KEY.
|
||||
- `video port RX full/drained`: заполнение и полная разгрузка encoded FIFO перед возобновлением
|
||||
приёма. Hysteresis переносит ограничение до новых входов sender encoder, освобождая запас для догона.
|
||||
- `call audio TX timing`: ожидание g_mtx, обработка/кодирование под ним, ожидание posted task;
|
||||
post включает ожидание lock при выполнении задачи, его разрешение 100мкс. На Android этот же
|
||||
замер обработки включает программный AEC, на desktop AEC уже выполнен в capture worker.
|
||||
- `call audio TX queue`: число ожидающих кадров, возраст старейшего и avg/max ожидания кадров,
|
||||
переданных роутеру за интервал. Отправка роутеру не является подтверждением доставки пиру.
|
||||
- Android `audio capture latency`: возраст первого сэмпла перед native feed и число измеренных
|
||||
привязок; дополнительные поля старта устройств разделяют min-buffer query, constructor, routing и buffer setup.
|
||||
Возраст PCM содержит часть измеренной обработки: эти числа нельзя механически складывать.
|
||||
PTS/возраст используют локальный MONOTONIC, интервалы внутри native — локальный get_time_us/get_time_tb;
|
||||
эти временные базы и часы разных устройств напрямую не вычитаются. Все сводки формируются вне device callback.
|
||||
Период захвата 20мс и новые измерения требуют контрольного звонка; дополнительная desktop PCM-очередь,
|
||||
жизненный цикл подготовки Android и таймер показа видео этим изменением не менялись.
|
||||
|
||||
## 7. Адаптивный джиттер-буфер (SoundTouch)
|
||||
На Android `CallPlaybackBuffer` подбирает рабочую очередь отдельно от JB/SoundTouch:
|
||||
после 30с наблюдаемого продвижения playback head без underrun пробует уменьшить её на один
|
||||
блок PCM (960 сэмплов, 20мс), до одного блока или фактического минимума устройства.
|
||||
При росте AudioTrack.underrunCount добавляет один блок в пределах выделенной ёмкости.
|
||||
Интервал следующей пробы удваивается (60/120/240/300с), чтобы не повторять частые неудачные уменьшения.
|
||||
После старта, запроса/подтверждения маршрута и увеличения даются 2с на стабилизацию;
|
||||
после уменьшения первый underrun учитывается немедленно. Отсутствие продвижения head или
|
||||
пропуск наблюдений >1с обнуляет стабильный участок. Сброс системного счётчика учитывается отдельно.
|
||||
Фактическая смена выхода начинает подбор с 40мс заново. Непринятая меньшая заявка фиксирует
|
||||
нижнюю границу до следующего выхода; ошибки API выключают подбор с WARN/ERROR, сохраняя воспроизведение.
|
||||
Reporter управляет размером под trackLock независимо от фильтра debug; playback не берёт этот lock.
|
||||
Нет flush/recreate, частичная запись PCM сохраняется. После смены размера/выхода через секунду
|
||||
повторно измеряется задержка для AEC. Ожидание полного буфера и недостача сети не увеличивают его размер.
|
||||
Лог настройки содержит reason, previous/requested/actualFrames и прирост underruns;
|
||||
достижение ёмкости предупреждает не чаще раза в 10с. Принятие и стабильность 20мс ещё требуют
|
||||
проверки на Samsung; буферы HAL и Bluetooth могут добавлять собственную задержку.
|
||||
На desktop адаптация устройства ещё не реализована: выход принадлежит SoundManager,
|
||||
изменение backend-буфера должно выполняться его владельцем, а не отдельным звонком.
|
||||
|
||||
Буфер ≤ **1000 мс**, target playout ~60 мс; при росте буфера плавно tempo → **1.6x**.
|
||||
Диагностика Android при `call=debug`: playback сохраняет один числовой снимок на блок (20мс)
|
||||
в кольце 512 элементов, без форматирования/вывода. Отдельный reporter раз в 200мс забирает записи;
|
||||
раз в секунду выводит `audio summary` с min/avg/max очередей, задержек pull/write и опоздания потока,
|
||||
целевым запасом/разбросом доставки JB, темпом, недостающим PCM и отдельными счётчиками JB/AudioTrack.
|
||||
Снимок JB не сбрасывает события для CALL_STATS. encodedMs — очередь Opus после pull,
|
||||
processingMs — оставшийся PCM SoundTouch в длительности исходной речи; reserveMs зависит только от доставки.
|
||||
Reporter опрашивает AudioTimestamp, routedDevice, underrunCount и размеры буфера не чаще раза в 200мс.
|
||||
Playback читает только опубликованный неизменяемый снимок. Его begin/end и возраст deviceAgeUs
|
||||
отличают задержку опроса от задержки аудио; stampQueryUs/routeQueryUs/propertiesQueryUs показывают стоимость запросов.
|
||||
Android API может удерживать общие внутренние locks даже при вызове из reporter: перенос убирает
|
||||
прямое ожидание диагностического запроса в playback, но сам по себе не доказывает отсутствие влияния на write/head.
|
||||
frame/time аппаратного timestamp и время начала опроса печатаются отдельно;
|
||||
при недоступности стоит stampOk=0. Времена trace — MONOTONIC; очередь устройства — written-played.
|
||||
Эти показатели нельзя складывать с возрастом AudioTimestamp как независимые задержки.
|
||||
В playback отдельно измеряются writeCallUs (только вызовы write), headCallUs (чтение позиции),
|
||||
progressCallUs (публикация позиции ядру), jbSnapshotUs (нативный снимок), writeSpanUs (весь цикл записи)
|
||||
и blockUs (pull до формирования строки). При частичных записях длительности вызовов суммируются за блок.
|
||||
pullGapExcessUs — превышение интервала между началами соседних pull над 20мс;
|
||||
включает ожидание устройства, а не только задержку планировщика (прежнее поле wakeLateUs удалено).
|
||||
Старое поле writeUs включало чтение позиции и прогресс: его нельзя трактовать как длительность одного AudioTrack.write.
|
||||
Stop дожидается reporter, включая непрерываемый Android-запрос, до освобождения AudioTrack.
|
||||
|
||||
`audio detail` сохраняет 2с до и 3с после старта, фактической смены routedDevice или новой аномалии:
|
||||
очередь устройства >100мс в течение секунды, отсутствие продвижения head 100мс при наличии данных,
|
||||
либо новый episode underrun (затухает через 3с без повторений). Порог очереди — диагностический,
|
||||
не настройка JB и не изменение воспроизведения. События внутри окна объединяются, между выгрузками
|
||||
не менее 10с; продолжающаяся аномалия не создаёт повторных дампов. Восстановление логируется INFO.
|
||||
Подробные строки содержат по четыре снимка и заголовок с порядком колонок; при stop окно может быть неполным.
|
||||
Переполнение кольца при задержке reporter имеет отдельный WARN lost=N. Аудиоданные не сохраняются.
|
||||
Выключение call=debug прекращает сбор/выгрузку; буфер и reporter освобождаются с playback worker.
|
||||
Сравнивать сводки следует по call ID и monoUs; рост сетевого запаса сам по себе не является ошибкой.
|
||||
|
||||
## 7. Буфер звонка и восстановление
|
||||
|
||||
`call_jitter` независим от `radio_jitter`: рация сохраняет свои правила задержки и сброса.
|
||||
Буфер звонка хранит до 750 кодированных кадров (15с): 10с перебоя и 5с запаса.
|
||||
Целевой запас — `clamp(max-min, 60, max_reserve_ms)` мс по изменению задержки доставки:
|
||||
на входящем кадре накапливается разность интервалов приёма и отправки, десять секундных интервалов хранят её экстремумы.
|
||||
Отправитель уже передаёт timestamp в CALL_MEDIA; смещение часов пиров исключается, переполнение uint32 учитывается.
|
||||
Подготовка местного аудиоустройства и ускоренное воспроизведение очереди не изменяют оценку доставки.
|
||||
Минимум/максимум глубины очереди для GUI собираются отдельно в аудиопотоке за те же десять секунд.
|
||||
Верхний предел задаётся `call_jitter_max_reserve_ms`: 500–2000 мс, по умолчанию 1000 мс.
|
||||
Настройка есть в аудиопараметрах desktop и Android и применяется к следующему звонку.
|
||||
Начальное и повторное накопление ждёт этот запас; увеличение запаса не прерывает текущую речь.
|
||||
Ускорение зависит от превышения запаса. При отсутствии входящих кадров обычное воспроизведение
|
||||
может исчерпать запас: это не жёсткий запрет воспроизведения ниже цели.
|
||||
Статистика раз в секунду отображается в desktop-окне и Android-экране звонка (под кнопками): глубина, запас, минимум/максимум,
|
||||
темп, RTT, удаления при переполнении и недостачи PCM. Диагностика буфера — категория `call=debug`.
|
||||
После восстановления кадры последовательно декодируются; темп плавно растёт до 1.6x
|
||||
без изменения высоты голоса: линейный предел роста 1.0x→1.6x за 500мс воспроизведения.
|
||||
При уменьшении накопления целевой темп снижается к 1.0x; скорость снижения ограничена тем же пределом.
|
||||
Пауза аудиоустройства не сбрасывает буфер. Переполнение удаляет старейший кадр с WARN и счётчиком.
|
||||
|
||||
Роутер сохраняет последовательность и неподтверждённые данные при смене пути.
|
||||
Для сервиса звонка срок без прогресса ACK увеличен примерно до 20с; прочие сервисы сохраняют прежний срок.
|
||||
Перезапуск удалённого процесса/роутерной сессии не является кратковременным перебоем пути и может сбросить данные.
|
||||
SoundTouch: `USE_QUICKSEEK=1, SEQUENCE_MS=20, SEEKWINDOW_MS=10, OVERLAP_MS=4`.
|
||||
API: `ajb_create / ajb_push / ajb_pull / ajb_stats / ajb_destroy`.
|
||||
|
||||
## 8. Тоны и диагностика
|
||||
|
||||
- **Сталл >500мс** (нет входящих медиа) → в `call_pull_pcm` микшируется
|
||||
- **Исходящий звонок — две фазы с разными гудками** (у вызывающего):
|
||||
- фаза 1 «пытаемся связаться» (`OUTGOING`: INVITE отправлен/ретранслируется, маршрут
|
||||
устанавливается) — гудок «соединение» (короткие бипы);
|
||||
- фаза 2 «звоним» (`RINGING`: получен RINGING, у пира идёт вызов) — длинные гудки (ringback).
|
||||
Гудки генерируются платформенно-нативно: desktop — цикличные MP3 через `SoundManager`
|
||||
(`call_connecting`/`call_ringing`), Android — цикличные MP3 через `CallRingback`/`MediaPlayer`.
|
||||
Переключение — по `CHAT_EVT_CALL_RINGING`,
|
||||
остановка — по `CALL_ACCEPTED`/`CALL_ENDED`/`CALL_ERROR`/hangup.
|
||||
- **Начальное ожидание первого PCM** → тишина без сигнала потери звука. Подготовка удалённого микрофона
|
||||
и начальное накопление jitter не считаются перебоем. Существующий таймаут 20с без медиа действует с ACTIVE.
|
||||
- **Недобор PCM ≥500мс после начала воспроизведения** → в `call_audio_pull_pcm` воспроизводится
|
||||
`CALL_TONE_GLITCH`: `пи-пи (2×150мс, gap 100мс) … тишина до 1.0с … пи(200мс)
|
||||
пауза(200мс) пи(200мс)`, 1000 Гц, цикл пока длится сталл.
|
||||
пауза(200мс) пи(200мс)`, 1000 Гц, цикл пока длится сталл. До порога — тишина при нехватке PCM.
|
||||
При возобновлении PCM сигнал останавливается; одного входящего пакета без достаточного запаса недостаточно.
|
||||
- **Завершение звонка → всегда** `CALL_TONE_ENDED`: 3 нисходящих 700/500/400 Гц по 150 мс.
|
||||
- Счётчики: tx/rx кадры, `c_tx_dropped`/`c_rx_dropped`, дропы буфера, сталлы,
|
||||
tempo-события, причины завершения. Логи `DEBUG_CATEGORY_CALL` (+стата раз в 1с).
|
||||
@@ -142,32 +389,50 @@ CHAT_EVT_CALL_RINGING 33 [call_id:8]
|
||||
CHAT_EVT_CALL_ACCEPTED 34 [call_id:8]
|
||||
CHAT_EVT_CALL_ENDED 35 [call_id:8][reason:1]
|
||||
CHAT_EVT_CALL_DECLINED 36 [call_id:8][reason:1]
|
||||
CHAT_EVT_CALL_STATS 37 [call_id:8][rtt_ms:2][buffer_ms:2][tempo_x100:2]
|
||||
CHAT_EVT_CALL_STATS 39 [call_id:8][rtt:2][buffer:2][tempo:2][dropped:2][underruns:2][min:2][max:2][reserve:2]
|
||||
CHAT_EVT_CALL_ERROR 38 [call_id:8][err:1][text:var]
|
||||
CHAT_EVT_CALL_AUDIO_ROUTE 39 (Android: текущий маршрут/наличие гарнитуры)
|
||||
```
|
||||
|
||||
## 10. Публичный API (call.h)
|
||||
## 10. Основные функции публичного API
|
||||
|
||||
```
|
||||
int call_start(ch_id, peer_node_id, out_call_id); // caller (post в uasync)
|
||||
void call_accept(call_id); // callee
|
||||
void call_decline(call_id);
|
||||
void call_hangup(call_id);
|
||||
int call_is_online(ch_id, peer_node_id); // BGP-проверка для UI
|
||||
// аудио-поток (общий процессинг в C-ядре):
|
||||
int call_encode_pcm(call_id, pcm, n, opus_out, cap); // single-writer: audio thread
|
||||
int call_send_encoded(call_id, opus, len); // post → uasync → tx_q + flush
|
||||
int call_pull_pcm(call_id, pcm_out, max); // single-writer: audio thread (ajb+tones)
|
||||
void call_release(call_id); // post → uasync: освободить аудио-объекты
|
||||
`call.h`: сигналинг и отправка готового Opus выполняются в uasync-потоке.
|
||||
Из других потоков команды передаются через `uasync_post`/мост GUI и соответствующие `call_*_trampoline`.
|
||||
`ch_id` — идентификатор CHAT-группы либо `dm:<conv_id>`; `out_call_id` — значение, заранее сгенерированное вызывающим.
|
||||
|
||||
```c
|
||||
void call_start(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id, uint64_t out_call_id);
|
||||
void call_accept(struct UTUN_INSTANCE* inst, uint64_t call_id);
|
||||
void call_decline(struct UTUN_INSTANCE* inst, uint64_t call_id);
|
||||
void call_hangup(struct UTUN_INSTANCE* inst, uint64_t call_id);
|
||||
int call_is_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id);
|
||||
int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t* opus, int len,
|
||||
const struct call_capture_time* timing);
|
||||
```
|
||||
|
||||
`call_init / call_destroy` — вызываются из `chat_core_init / chat_core_destroy`.
|
||||
`call_audio.h`: движок сам кодирует PCM и ставит отправку Opus в uasync.
|
||||
Захват и воспроизведение используют по одному аудиопотоку на направление.
|
||||
|
||||
```c
|
||||
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count, int64_t capture_pts_us);
|
||||
int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask, int64_t capture_pts_us);
|
||||
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples);
|
||||
void call_audio_release(uint64_t call_id);
|
||||
```
|
||||
|
||||
`feed_pcm` принимает ровно 960 отсчётов; `feed_prepared_pcm` — 960 отсчётов после платформенного AEC
|
||||
и маску mute. `call_audio_release` вызывается в GUI-потоке после остановки обоих направлений I/O.
|
||||
Создание движка — `call_audio_start`, для подготовленного PCM — `call_audio_start_prepared`.
|
||||
Видео настраивается через `call_video_configure`, кадры отправляются через `call_video_send_frame`
|
||||
в uasync-потоке; полный контракт — в `call_video_transport.h`.
|
||||
|
||||
`call_init / call_destroy` вызываются из `chat_service_start / chat_service_stop` в `utun_instance.c`:
|
||||
инициализация после `dm_core_init`, освобождение перед `dm_core_destroy`.
|
||||
|
||||
## 11. Платформенный I/O
|
||||
|
||||
- **Desktop:** `tools/chatgui/src/call_audio_engine.cpp` — miniaudio full-duplex
|
||||
(capture → `call_encode_pcm`, `call_pull_pcm` → playback) + окно звонка (UI)
|
||||
(capture через VoiceAudioIo → `call_audio_feed_prepared_pcm`, `call_audio_pull_pcm` → playback) + окно звонка (UI)
|
||||
+ события в `gui_bridge_impl.cpp`.
|
||||
- **Android:** JNI `nativeCall*` + Kotlin `AudioRecord`/`AudioTrack`.
|
||||
**Переключение источника:** `AudioManager` (API31+ `setCommunicationDevice` +
|
||||
@@ -184,7 +449,8 @@ void call_release(call_id); // post → uasync: о
|
||||
mmx_optimized, sse_optimized) как C++. `call.c` попадёт через GLOB `src/*.c`.
|
||||
- `tools/chatgui-android/libutun_lite/CMakeLists.txt` + `utun_sources.cmake`:
|
||||
`LANGUAGES C CXX`, те же файлы.
|
||||
- `src/transport_layer/etcp_api.h`: `#define ETCP_RT_ID_CALL 0x34`.
|
||||
- `src/transport_layer/etcp_api.h`: `ETCP_RT_ID_CALL=0x36`, `ETCP_RT_ID_CALL_VIDEO_CONTROL=0x3D`,
|
||||
`ETCP_RT_ID_CALL_VIDEO=0x3E`, `ETCP_RT_ID_CALL_AUDIO=0x3F`.
|
||||
- `lib/debug_config.h`: `DEBUG_CATEGORY_CALL 31`, `DEBUG_CATEGORY_COUNT` → 32.
|
||||
|
||||
## 13. Порядок задач
|
||||
|
||||
+201
-66
@@ -1,93 +1,228 @@
|
||||
# DM — прямой p2p чат между двумя пользователями группы
|
||||
# DM: доставка сообщений и файлов
|
||||
|
||||
## 1. Цель
|
||||
Личные беседы принадлежат `src/dm/`. Общие CHAT-группы используются для знакомства,
|
||||
проверки ролей и маршрутизации; группа не становится идентичностью беседы.
|
||||
|
||||
Возможность начать личный чат с любым пользователем доступных групп.
|
||||
Пока target offline сообщения временно хранятся на storage-узлах.
|
||||
## Реализовано
|
||||
|
||||
## 2. Ключевые решения
|
||||
- История и постоянная исходящая очередь сообщений в общей SQLite ядра.
|
||||
- Доставка через живой маршрут router, включая BGP-транзит.
|
||||
- При отсутствии маршрута — подписанный PUT доступному суперузлу общей группы сторон.
|
||||
- Приём после проверки подписи и расшифровки, подтверждение только после commit.
|
||||
- Доставка с суперузла после возвращения получателя, независимо от отправителя.
|
||||
- Точное удаление промежуточного сообщения и сохранение квитанции для поздних повторов.
|
||||
- Подготовка, шифрование, проверка и durable публикация медиа в рабочих потоках.
|
||||
- Прямая передача файлов либо custody на отдельном M, доставка после restart и подтверждённое удаление.
|
||||
- TTL и квота M; вложения и статусы доставки в Qt и Android.
|
||||
- Голосовые Opus и видео MP4 с зашифрованными метаданными; общий ввод и отображение с каналами.
|
||||
|
||||
- Отдельная DM-подсистема (НЕ канал: без TOPO_GROUP/member_sync/merkle).
|
||||
- Идентичность детерминированная: одна ветка на пару, согласование без handshake.
|
||||
- E2E-шифрование содержимого сразу.
|
||||
- Offline-хранение: dm_mailbox на storage-узлах с флагом dm_storage.
|
||||
- Доставка: fallback — сперва проверка наличия узла в BGP-группе; доступен →
|
||||
прямой send; нет связи → storage.
|
||||
## Идентичность и шифрование
|
||||
|
||||
## 3. Идентичность и ключи (детерминированные)
|
||||
```text
|
||||
conv_hash = SHA256("utun_dm_v" || min(node_a,node_b) || max(node_a,node_b))
|
||||
conv_id = native_u64(conv_hash[0:8]) & 0x7FFFFFFFFFFFFFFF
|
||||
content_key = SHA256(X25519(my_private, target_public) || "utun_dm_content")
|
||||
msg_nonce = SHA256(conv_id || author || seq)[..13]
|
||||
media_nonce = SHA256("utun_dm_media" || author || media_id || part)[..13]
|
||||
```
|
||||
|
||||
conv_id = SHA256("utun_dm_v1" || min(node_a,node_b) || max(node_a,node_b))[..63]
|
||||
shared = X25519(my_x25519_priv, peer_x25519_pub)
|
||||
content_key = SHA256(shared || "utun_dm_content")
|
||||
Префикс conv_hash — 9 ASCII-байт без NUL. ID узлов подаются в хеш в порядке
|
||||
байтов хоста; native_u64 читает первые 8 байт хеша в том же порядке.
|
||||
Нулевой conv_id заменяется на 1.
|
||||
|
||||
Обе стороны выводят одинаковые значения из своего privkey и pubkey пира
|
||||
(известен из общей группы). Смена x25519 у пира = новая ветка.
|
||||
Сообщения и медиа используют один content_key target, без ключей отдельных файлов.
|
||||
AES-256-CCM: nonce 13 байт, tag 16 байт. Максимальная порция plaintext — 65535 байт;
|
||||
порция медиа — 32768 байт. UUID медиа новый для каждой подготовки. Повтор доставки
|
||||
использует неизменяемый ciphertext. Повторная подготовка после ошибки требует нового UUID.
|
||||
|
||||
## 4. Модель данных (SQLite, общий chats.db)
|
||||
Каноническое тело сообщения:
|
||||
|
||||
dm_conversations(conv_id TEXT PK, peer_node_id, peer_x25519 BLOB, peer_ed25519 BLOB,
|
||||
peer_name TEXT, last_out_seq INT, last_in_seq INT, created_at INT, last_ts INT)
|
||||
dm_messages(conv_id TEXT, dir INT, seq INT, ts INT, author INT, ct TEXT,
|
||||
data BLOB, sig BLOB, PRIMARY KEY(conv_id,dir,seq))
|
||||
dm_mail(recipient INT, sender INT, conv_id TEXT, seq INT, ts INT, data BLOB, sig BLOB,
|
||||
ttl INT, PRIMARY KEY(recipient,sender,seq))
|
||||
```text
|
||||
[conv:8][seq:8][ts:8][author:8][ct_len:1][ct][enc_len:2][ciphertext+tag][author_sig:64]
|
||||
```
|
||||
|
||||
Два независимых направленных потока (A→B, B→A) с монотонным seq.
|
||||
Без chain-хеша: для 2 пиров достаточно seq + catch-up по диапазону.
|
||||
Автор подписывает все поля до sig. conv связывает автора с получателем.
|
||||
Для `ct=file` перед подписью добавляется descriptor `[UUID:16][plaintext_size:8][SHA256(ciphertext):32]`.
|
||||
В зашифрованном теле находится `attachment_info`; пути хранения строятся только из UUID.
|
||||
Размер plaintext сообщения ограничен 1024 байтами. seq — положительное int64.
|
||||
|
||||
## 5. Доставка
|
||||
```text
|
||||
[kind:1][name_len:2][duration_ms:4][width:2][height:2][UTF8_name:name_len][waveform:100, только voice]
|
||||
```
|
||||
|
||||
1. Проверка доступности: peer присутствует в BGP-группе (общая группа).
|
||||
2. Доступен → прямой send (сервис ETCP_RT_ID_DM = 0x34), ждём DM_ACK{conv_id,seq}.
|
||||
Нет ACK за таймаут → fallback в mailbox.
|
||||
3. Нет связи → MAIL_PUT на 1..N storage-узлов.
|
||||
4. Catch-up при connect: DM_HELLO{conv_id,last_out_seq,last_in_seq} → пир досылает
|
||||
(in_seq+1 .. out_seq).
|
||||
5. Dedup: вставка только при seq > last_in_seq.
|
||||
Поля метаданных многобайтной длины имеют big-endian формат. kind: 1=file,
|
||||
2=voice, 3=video. Имя — basename до 255 байт, без разделителей, управляющих символов
|
||||
и некорректного UTF-8. FILE имеет нулевые duration/dimensions; VOICE — положительную
|
||||
длительность и 100 символов уровней waveform; VIDEO — длительность и размеры.
|
||||
Длительность ограничена 24 часами, размеры — 16384. Метаданные проверяются до
|
||||
локального commit и до подтверждения получателем; S/M видят только ciphertext.
|
||||
|
||||
## 6. E2E-шифрование
|
||||
Медиа ciphertext — последовательность порций с отдельным tag каждой порции.
|
||||
Последняя порция короче; пустой файл содержит один tag. Размер plaintext и SHA256
|
||||
ciphertext должны входить в подписанный manifest. Потоковая расшифровка проверяет все
|
||||
tags, точный размер и хеш; владелец публикует временный файл только после успеха
|
||||
и собственного durable сохранения.
|
||||
|
||||
data_enc = nonce(13B, счётчик по seq) || AES-CCM_ct || tag
|
||||
## Владение и данные
|
||||
|
||||
secure_channel уже умеет AES-CCM. Storage/relay видят только шифротекст.
|
||||
- `dm_core`: беседы, история, outbox и подтверждение получателя.
|
||||
- `dm_mailbox`: промежуточные сообщения суперузла и квитанции.
|
||||
- `dm_crypto`: вывод conv_id, content_key и nonce сообщений без БД/сети.
|
||||
- `media_crypto`: общие CCM и потоковые файловые примитивы для DM и групповых вложений.
|
||||
- `dm_media`: файлы endpoint, загрузки и отдельная custody M.
|
||||
- `dm_mailbox_media`: durable задания S, выбор M и повтор удаления до DELETE_ACK.
|
||||
- `file_transfer`: подписанная передача байтов, backpressure и отдельный CM handle каждой операции.
|
||||
- `attachment`: формат, валидация и тип метаданных, без сети и БД.
|
||||
- `voice_file`: общий Opus-кодек отправки; `attachment_send` готовит файл в worker
|
||||
и передаёт результат отдельно в `chat_core` либо `dm_media`.
|
||||
|
||||
## 7. Storage-узлы (dm_storage)
|
||||
```text
|
||||
dm_conversations(conv_id, peer_node_id, peer_x25519, peer_ed25519, peer_name,
|
||||
group_id, last_out_seq, last_in_seq, created_at, last_ts)
|
||||
dm_messages(conv_id, dir, seq, ts, author, ct, data, sig)
|
||||
dm_outbox(conv_id, seq, body, retry_at)
|
||||
dm_pending_messages(recipient, sender, seq, msg, group_id, retry_at)
|
||||
dm_mail_receipts(recipient, sender, seq, receipt, group_id)
|
||||
```
|
||||
|
||||
- Новый флаг dm_storage=yes в adm_tags (блок B владельца), распространяется
|
||||
существующим member_sync без нового синка.
|
||||
- DM читает adm_tags мемберов общих групп (peers_<ch>) и кэширует storage-узлы.
|
||||
- Протокол mailbox (сервис ETCP_RT_ID_DM_MAILBOX = 0x35):
|
||||
Запись исходящего сообщения, увеличение seq и запись неизменяемого body в outbox —
|
||||
одна транзакция. При ошибке откатывается всё, сообщение не выходит в сеть.
|
||||
Приём, создание/обновление беседы и история — также одна транзакция.
|
||||
|
||||
MAIL_PUT {recipient,sender,conv_id,seq,ts,data,sig}
|
||||
MAIL_PULL {recipient, per-sender cursor}
|
||||
MAIL_ACK {up_to_seq}
|
||||
Dedup — точный ключ (conv,dir,seq) с проверкой сохранённой подписи. last_in_seq —
|
||||
статистика максимального номера, не курсор подтверждения: seq=99 после seq=100
|
||||
сохраняется. Тело с другой подписью для уже сохранённого seq отклоняется.
|
||||
|
||||
TTL 7 дней + лимит объёма, dedup по (recipient,sender,seq).
|
||||
У каждой локальной очереди таймер 1с; порция обработки до 32 задач, повтор задачи
|
||||
через 5с. retry_at предотвращает голодание хвоста очереди. Перед отправкой проверяется
|
||||
backpressure router. Потеря пути сохраняет durable задачу; остановка отменяет таймеры,
|
||||
но сохраняет БД. При старте очередь продолжает работу.
|
||||
|
||||
## 8. Старт и anti-spam
|
||||
## Подтверждения и протокол
|
||||
|
||||
- dm_start(peer): создать беседу (вывести conv_id/ключи), обеспечить прямое
|
||||
соединение (переиспользовать chat_sync_connect_node/node_conn_direct).
|
||||
- Пир автопринимает на первом DM_HELLO/сообщении, только если отправитель в
|
||||
общей группе.
|
||||
Сервис `ETCP_RT_ID_DM=0x34`: MSG=1, RECEIPT=2.
|
||||
|
||||
## 9. Файлы и интеграция
|
||||
```text
|
||||
["DMACK001":8][conv:8][seq:8][author:8][recipient:8][SHA256(message):32][recipient_sig:64]
|
||||
```
|
||||
|
||||
| Файл | Назначение |
|
||||
|------|-----------|
|
||||
| src/dm/dm_core.c/h | беседы, send/recv, таблицы, bind 0x34, conn-события, fallback |
|
||||
| src/dm/dm_crypto.c/h | derive + AES-CCM |
|
||||
| src/dm/dm_mailbox.c/h | storage-узел, push/pull/ack, bind 0x35 |
|
||||
Квитанцию создаёт только получатель после commit. Подпись охватывает первые 72 байта.
|
||||
Отправитель удаляет outbox только после проверки подписи и точного хеша тела.
|
||||
История беседы сохраняется. Подтверждение транспорта router не заменяет квитанцию.
|
||||
|
||||
Плюс:
|
||||
Сервис `ETCP_RT_ID_DM_MAILBOX=0x35`:
|
||||
|
||||
- src/chat/chat_member.c — бит CHAT_MEMBER_FLAG_DM_STORAGE + парсинг dm_storage.
|
||||
- utun_instance.c — dm_core_init/destroy.
|
||||
- chat_core — API dm_start/send/list + события CHAT_EVT_DM_*.
|
||||
- headless CLI — dm list/send/messages.
|
||||
- src/Makefile.am — новые источники.
|
||||
```text
|
||||
PUT=1 [recipient:8][message]
|
||||
DELIVER=2 [message]
|
||||
RECEIPT=3 [receipt:136]
|
||||
STORED=4 [conv:8][seq:8]
|
||||
```
|
||||
|
||||
## 10. Сервисы ETCP
|
||||
Все пакеты идут с подписью router; unsigned control отклоняется.
|
||||
PUT принимается только от автора, при роли supernode=yes у локального узла
|
||||
в общей группе автора и получателя. Роль берётся из подписанного adm_tags;
|
||||
node_type=4 — производная сетевой классификации, для авторизации не используется.
|
||||
storage=yes само по себе не даёт права хранить сообщения.
|
||||
|
||||
ETCP_RT_ID_DM = 0x34
|
||||
ETCP_RT_ID_DM_MAILBOX = 0x35
|
||||
STORED подтверждает durable запись суперузлом, outbox до квитанции получателя остаётся
|
||||
у автора. Суперузел повторяет DELIVER при появлении живого маршрута. RECEIPT проверяется
|
||||
по точному сохранённому телу; запись квитанции и удаление копии — одна транзакция.
|
||||
Квитанция пересылается автору. Поздний PUT возвращает её и не возрождает задачу.
|
||||
|
||||
dm_mail_receipts содержит только служебные квитанции; доставленное содержимое
|
||||
с промежуточного узла удаляется. Ранее использовавшийся dm_mail/PULL/HELLO и
|
||||
диапазонные ACK не поддерживаются: это devel, совместимость не сохраняется.
|
||||
TTL текстовых сообщений не введён; недельный TTL из pm.txt относится к медиа.
|
||||
|
||||
## Доставка медиа по pm.txt
|
||||
|
||||
1. Общая передача байтов: явные peer/group/file/size, backpressure и CM handle
|
||||
на всю активную операцию. Владение handle сохраняется после DOWN/TIMEOUT.
|
||||
Используется `ETCP_RT_ID_FILE_TRANSFER=0x37`. Один callback читает/отправляет
|
||||
не более 8 КиБ, продолжение через `call_soon`; inflight ограничен 16 пакетами.
|
||||
2. Отдельный owner dm_media: подготовка ciphertext, manifest, отправка/скачивание,
|
||||
временные файлы, TTL и лимит хранения. Каталоги dm_media/ и dm_pending/ вне
|
||||
группового обхода очистки orphan-файлов; групповые объявления HAVE_BLOCK не используются.
|
||||
3. Суперузел сначала durable регистрирует сообщение и media job, затем выбирает M.
|
||||
M скачивает ciphertext у A и сообщает S подтверждённое хранение, место и expiry.
|
||||
4. Текстовая квитанция и квитанция медиа независимы. Получатель подтверждает медиа
|
||||
после проверки, публикации файла и commit. S хранит задачу удаления у M до DELETE_ACK;
|
||||
поздний STORED не возрождает завершённую доставку.
|
||||
5. TTL медиа `dm_media_ttl_sec` по умолчанию 604800 с; квота `dm_media_storage_mb`
|
||||
по умолчанию 1024 МиБ. Настройки на M. Новая custody отклоняется при
|
||||
недостатке квоты; уже подтверждённые копии не вытесняются молча раньше срока.
|
||||
6. Stop снимает новые задачи и закрывает transfer/CM handles, затем ждёт
|
||||
media_async с живыми owner-контекстами. Группы уничтожаются после owners.
|
||||
|
||||
Control `ETCP_RT_ID_DM_MEDIA=0x38`: OFFER, STORE, STORED, RECEIPT, DELETE,
|
||||
DELETE_ACK, EXPIRED, REJECT; точные форматы заданы в `dm_media.h` и реализациях owners.
|
||||
Подпись router обязательна. Media receipt содержит `DMFILE01`, conv, seq, author,
|
||||
recipient, UUID и SHA256 полного подписанного сообщения; B подписывает первые 88 байт.
|
||||
Отдельные таблицы `dm_files`, `dm_custody`, `dm_custody_done`, `dm_media_jobs`,
|
||||
`dm_media_holders` сохраняют состояния и tombstones. Все выданные grants M
|
||||
учитываются при удалении; повторный STORE не продлевает TTL.
|
||||
|
||||
Qt и Android получают историю через события ядра: 41 — conv + JSON сообщений,
|
||||
45 — JSON бесед. Снимки ограничены 200 записями. Отправка вложения вызывает
|
||||
`dm_send_file` в uasync; длительная обработка выполняется в `media_async`.
|
||||
Android копирует выбранный URI и сохраняет полученный файл через `Dispatchers.IO`;
|
||||
Qt сохраняет файл из контекстного меню в отдельном потоке. Выбор «как файл» сохраняет
|
||||
оригинал; «как видео» в Qt использует общий FFmpeg worker и метаданные конечного MP4.
|
||||
Android записывает MP4 через CameraX; импорт MP4 проходит через платформенный
|
||||
MediaMetadataRetriever в IO. Другие контейнеры на Android отправляются как файлы.
|
||||
|
||||
Голосовая запись отдаёт worker собственный PCM либо компрессор без копирования
|
||||
в GUI/uasync; worker выполняет flush, Opus-кодирование и вычисление waveform.
|
||||
Кодирование Qt и Android использует один `voice_file`. Адресат фиксируется до
|
||||
записи, выбора URI и подготовки; завершение не читает текущую беседу GUI.
|
||||
Подготовленный PM-файл и Android cache удаляются после копирования, ошибки или отмены;
|
||||
пользовательский исходник Qt остаётся во владении пользователя.
|
||||
|
||||
Событие 46 `[target_len:1][target][job_UUID:16][state:1]` сообщает начало (0),
|
||||
готовность (1) и ошибку (2). Target канала — числовой ID, PM — `dm:<conv_id>`.
|
||||
Подготовка, доставка и воспроизведение имеют независимые состояния. UI отображает
|
||||
только задания текущей беседы. Состояния файла и delivery receipt приходят в JSON
|
||||
41; `content_type`, duration, dimensions и waveform преобразуются в общие роли
|
||||
сообщения. Канальные скачивания вызываются контроллером только для каналов;
|
||||
PM-файлы доставляются автоматически через `dm_media`.
|
||||
|
||||
Android использует общие `ConversationComposer`/`ConversationMessages` для обеих
|
||||
бесед; Qt — `InputBar` и `MessageDelegate`. Превью Qt создаются в общем пуле
|
||||
с двумя workers; декодирование длинного Opus-файла выполняется вне GUI. Оба плеера
|
||||
проверяют заголовок, пакеты и end marker; Qt ограничивает PCM буфер 256 МиБ.
|
||||
|
||||
## Проверки
|
||||
|
||||
`test_dm`: симметрия ключа target, CCM round-trip, пустое тело, пределы длины,
|
||||
неверный ключ/tag, разделение nonce; потоковые файлы на границах 32 КиБ,
|
||||
неверные author/UUID/hash, обрезание и лишние байты.
|
||||
|
||||
`test_dm_e2e`: реальные A–C–B соединения и BGP; нет физического A–B линка.
|
||||
Проверяет out-of-order, dedup/conflict, подписи, SQL rollback при приёме и enqueue,
|
||||
ACK чужого тела, отказ mailbox без роли, точное удаление и поздний PUT.
|
||||
Первый сценарий передаёт 40 offline-сообщений (больше порции pump) и перезапускает источник с pending outbox; второй перезапускает
|
||||
суперузел и доставляет первое сообщение при выключенном источнике.
|
||||
|
||||
`test_dm_media`: реальные A/B/S/M с раздельными ролями supernode и storage;
|
||||
прямая передача, custody, restart S/M при выключенном A, точный файл на B,
|
||||
удаление копии и тела S, поздние STORE/STORED, TTL, отказ по квоте, неверная квитанция,
|
||||
SQL rollback регистрации, состояния JSON и отмена операции с CM handle.
|
||||
Голосовое передаётся напрямую; метаданные видео сохраняются после offline custody
|
||||
и restart S/M при выключенном источнике.
|
||||
|
||||
`test_dm_worker`: повторное шифрование 8 МиБ не менее 0,5 с и проверка расшифровки;
|
||||
work выполняется вне uasync, done в его потоке, heartbeat продолжает работать.
|
||||
Destroy отменяет уже posted completion, callback вызывается ровно один раз.
|
||||
|
||||
`test_attachment`: строгий формат, повреждения и граничные длины, реальное
|
||||
кодирование/декодирование 50 Opus-кадров, длительность и waveform.
|
||||
`test_attachment_send`: выбранная беседа после смены текущего target, освобождение
|
||||
PCM в worker, остановка до completion без регистрации и удаление отклонённого cache.
|
||||
Qt `test_voice_file`: реальные PCM/Opus, повреждённые файлы и отмена async decoder.
|
||||
|
||||
Проверка 01.10.2026: чистые сборки Qt и Android APK; полный `check.sh` —
|
||||
117 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root), proxy/burst/load PASS.
|
||||
Qt CTest: 7 PASS, 0 FAIL; Android APK собран после `clean`.
|
||||
В тесте worker максимальный интервал heartbeat составил 10,3 мс.
|
||||
Интерактивная проверка GUI на телефоне и десктопе не выполнялась.
|
||||
@@ -0,0 +1,118 @@
|
||||
# Зашифрованные вложения группового чата
|
||||
|
||||
В desktop chatgui после выбора файла или перетаскивания в чат рядом с его именем
|
||||
появляется флажок «Зашифровать вложение». Он применяется к выбранному файлу,
|
||||
изображению или видео. Удаление, замена файла и переход в другую беседу сбрасывают
|
||||
флажок. Выбор фиксируется вместе с адресатом при отправке, до подготовки.
|
||||
В DM флажок скрыт: используется существующее автоматическое E2E-шифрование лички.
|
||||
|
||||
Android показывает зашифрованное вложение отдельной карточкой со статусом запроса
|
||||
ключа. После одобрения карточка обновляется на исходный файл, изображение, видео
|
||||
или голосовое. В интерфейс передаётся только расшифрованный путь; контейнер `.uenc`
|
||||
нельзя открыть или сохранить как готовое вложение. Цитаты и список чатов показывают
|
||||
название «Зашифрованное вложение» вместо manifest.
|
||||
|
||||
Автор генерирует независимые случайные crypto_UUID и ключ AES-256 на каждую
|
||||
подготовку. Ключ записывается в локальную SQLite до шифрования. Готовый контейнер
|
||||
передаётся через обычные media_index/media_delivery. UUID транспортного медиа
|
||||
и crypto_UUID имеют разные назначения: первый идентифицирует сетевые блоки,
|
||||
второй — контейнер и ключ. Повтор доставки передаёт прежний контейнер.
|
||||
|
||||
## Подписанное сообщение и контейнер
|
||||
|
||||
В подписанном сообщении `ct=application/x-utun-encrypted`, базовое имя тела —
|
||||
`<crypto_UUID:32hex>.uenc`. Остальные поля manifest описывают шифротекст:
|
||||
размер, SHA256 всего контейнера, UUID и подписи блоков. Ключ, исходное имя,
|
||||
MIME, waveform и остальные медиаметаданные в историю канала не записываются.
|
||||
|
||||
Контейнер:
|
||||
|
||||
```text
|
||||
magic "UTENC001":8
|
||||
crypto_UUID:16
|
||||
plaintext_size:8BE
|
||||
encrypted_metadata_size:2BE
|
||||
SHA256(encrypted_file_body):32
|
||||
encrypted_metadata:variable
|
||||
encrypted_file_body:variable
|
||||
```
|
||||
|
||||
Метаданные содержат MIME в 32 байтах и сериализованный `attachment_info`.
|
||||
Каждая часть шифруется AES-256-CCM с тегом 16 байт и nonce 13 байт.
|
||||
Nonce метаданных использует `part=UINT64_MAX`; файл — порции 32768 байт
|
||||
с номерами от нуля. Общие примитивы находятся в `media_async/media_crypto`;
|
||||
формат существующего DM при их выделении не менялся.
|
||||
|
||||
Получатель проверяет подписанный SHA256 полного контейнера, UUID, tag метаданных,
|
||||
все tags файла, точный размер и hash тела. Результат пишется во временный файл
|
||||
и публикуется после успешной проверки и durable rename. Все длительные файловые
|
||||
операции и криптография выполняются в media_async.
|
||||
|
||||
## Хранение и разрешения
|
||||
|
||||
Локальные таблицы не регистрируются в db_sync или Merkle:
|
||||
|
||||
- `attachment_keys`: ключ автора/получателя, связь с общим message_id,
|
||||
crypto_UUID, hash и пути двух копий, расшифрованные метаданные и состояние.
|
||||
- `attachment_key_requests`: один запрос на `(channel,message_id,peer)`,
|
||||
направление, решение и дата создания.
|
||||
- `attachment_key_outbox`: команда, адресат и время следующей попытки.
|
||||
|
||||
Шифротекст хранится в `media/<channel>/<crypto_UUID>.uenc`, локальная открытая
|
||||
копия — в `decrypted/<channel>/<crypto_UUID>.file`. В media_files и объявлениях
|
||||
держателей остаётся только шифротекст, включая backfill после restart.
|
||||
Квота медиакеша учитывает обе копии; очистка кеша не удаляет ключ. При повторном
|
||||
открытии удалённая открытая копия восстанавливается с сохранённым ключом.
|
||||
|
||||
После полной проверенной загрузки обычный клиент автоматически запрашивает
|
||||
ключ автора. Фоновая копия узла с подписанной ролью `storage=yes` запрос не
|
||||
создаёт; ручная пользовательская загрузка на таком узле запрашивает доступ.
|
||||
Сетевая классификация `peers.storage` для этого не используется.
|
||||
|
||||
В шапке канала кнопка «Запросы расшифровки» показывает число запросов без решения.
|
||||
Список содержит участника, вложение, дату и отдельные кнопки выдачи/отказа.
|
||||
Двойной клик переводит к исходному сообщению. После выдачи видны ожидание доставки
|
||||
и подтверждённая доставка ключа; отказ также сохраняется.
|
||||
|
||||
## Адресный протокол
|
||||
|
||||
Router-сервис `ETCP_RT_ID_ATTACHMENT_KEYS=0x42`. Каждый пакет обязан иметь
|
||||
`ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED`; промежуточные узлы ключа не видят.
|
||||
Протокол не создаёт личную беседу и не публикует служебные сообщения в канал.
|
||||
|
||||
```text
|
||||
magic "AK01":4 | command:1 | message_id:32 | crypto_UUID:16 | cipher_hash:32
|
||||
GRANT дополнительно содержит key:32
|
||||
|
||||
REQUEST=1, REQUEST_ACK=2, GRANT=3, DENY=4, RESULT_ACK=5
|
||||
```
|
||||
|
||||
Автор принимает запрос только на собственное опубликованное сообщение с
|
||||
сохранённым ключом. Участие запрашивающего проверяется по полному подписанному
|
||||
мемберу, join и дереву приглашений; удалённые мемберы отклоняются. Проверка
|
||||
повторяется при решении и перед каждой отправкой GRANT. Получатель принимает
|
||||
ответ только от автора, по своему существующему запросу и точной идентичности
|
||||
контейнера. Конфликтующие ключи и результаты отклоняются.
|
||||
|
||||
REQUEST_ACK отправляется после сохранения запроса. GRANT/DENY появляются
|
||||
в outbox только после сохранения решения. Полученный ключ сохраняется до
|
||||
RESULT_ACK и до запуска расшифровки. Повторы возвращают прежнее решение и не
|
||||
создают новых строк. Таймер обрабатывает ограниченную порцию очереди с
|
||||
backpressure и повтором через 5 секунд; restart продолжает очереди.
|
||||
|
||||
Для доставки нужен живой маршрут обеих сторон: офлайн-mailbox этого сервиса
|
||||
не реализован. Запрос ждёт появления автора; решение ждёт появления получателя.
|
||||
Уже выданный ключ невозможно отозвать у получателя. Защита содержимого не
|
||||
скрывает автора, канал, время и размер шифротекста. Автоматическая публикация
|
||||
транскрипции зашифрованного голосового в канал не запускается.
|
||||
|
||||
## Проверки
|
||||
|
||||
`test_encrypted_file`: границы порций, пустой файл, metadata roundtrip,
|
||||
неверные ключ/автор/UUID/hash, подмена метаданных с пересчитанным внешним hash,
|
||||
лишние байты и отсутствие частичного plaintext после отказа.
|
||||
|
||||
`test_encrypted_attachment`: реальные A–S–B/C, скачивание с S при выключенном A,
|
||||
перезапуски автора с запросами и получателя с ключом, выдача B и отказ C,
|
||||
отсутствие ключа на S/C, отрицательные control-пакеты, повтор GRANT,
|
||||
восстановление удалённого plaintext без нового согласия и удаление канала.
|
||||
+5
-1
@@ -1,4 +1,8 @@
|
||||
Архитектура etcp:
|
||||
> Исторический документ: описания NAT, модулей и ограничения обмена маршрутами ниже не отражают текущую реализацию.
|
||||
> Актуальные правила группового обмена маршрутами — в [topo_group.h](../src/routing_layer/topo_group.h),
|
||||
> модель узла — в [node_snapshot.md](node_snapshot.md), протокол роутера — в [etcp_router_doc.md](../src/routing_layer/etcp_router_doc.md).
|
||||
|
||||
Архитектура etcp:
|
||||
|
||||
ETCP устанавливает только одно подключение между двумя узлами. Это подключение может добавлять-убирать каналы связи (динамически) без разрыва соединения.
|
||||
Также каналы связи мониторятся и трафик распределяется между доступными каналами. Если канал определяется как недоступен - он исключается из обмена.
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
# Разбор полного прогона 2026-09-27
|
||||
|
||||
Исходный результат `./check.sh` вне песочницы: 92 passed, 5 failed, 1 skipped (98 тестов).
|
||||
Песочница запрещает сетевые сокеты; её результаты не использовались для оценки протокола.
|
||||
|
||||
Промежуточный прогон: 96 passed, 1 failed, 1 skipped; оставался `test_chat_join_e2e`.
|
||||
После KEY_REGISTER_ACK и исправления невыровненных чтений повторный полный
|
||||
`make check -j4`: **97 passed, 0 failed, 1 skipped (98)**.
|
||||
Лог: `/tmp/utun-ack-final-check.log`. Исходное падение отдельного root-теста и
|
||||
последующая доработка описаны ниже. После исправления autosockets общий набор дал
|
||||
97 PASS / 1 SKIP, а `check-proxy`, `check-burst`, `check-load` прошли:
|
||||
`/tmp/root-fix-full-check.log`.
|
||||
|
||||
`test_etcp_lifecycle`, `test_etcp_link_stress`, `test_conn_mgr_phases` дополнительно
|
||||
прошли ASan/UBSan/LeakSanitizer. Инструментированы тесты и изменённые транспортные/
|
||||
маршрутные модули, остальные объекты библиотек взяты из обычной сборки.
|
||||
Логи: `/tmp/ncd-late-asan.log`, `/tmp/link-role-asan.log`, `/tmp/cm-phases-asan.log`.
|
||||
|
||||
## Исправленные причины
|
||||
|
||||
- `test_conn_mgr_phases`: после TIMEOUT фазы DIRECT NCD навсегда подавлял UP,
|
||||
хотя conn_mgr сохранял handle для REVERSE. В логе физический ETCP уже UP,
|
||||
но conn_mgr продолжал DIRECT_REQ до своего таймаута. TIMEOUT теперь обозначает
|
||||
истечение срока первой попытки, CLOSED — окончательное завершение. Удерживаемый
|
||||
handle получает поздний UP. Отдельный lifecycle-регрессионный тест воспроизводит
|
||||
TIMEOUT → INIT/UP (одно уведомление) → DOWN → UP; до исправления он падал.
|
||||
- `test_etcp_link_stress`: ожидал reset всей сессии при коллизии дополнительного
|
||||
линка. Проверка заменена на сохранение эпох, отсутствие reset и дублей линков,
|
||||
доставку и ACK сообщений через новый линк. Прежний тест забирал сырые фрагменты
|
||||
вместо normalizer и мог считать служебные пакеты полезным трафиком. Теперь он
|
||||
использует отдельный ETCP service и проверяет каждый байт и порядок сообщений.
|
||||
Эта проверка выявила ошибку реализации: при принятии встречного INIT сохранённый
|
||||
исходящий линк не менял `is_server`. Роль обновляется при отправке INIT_RESPONSE.
|
||||
- `test_bgp_route_exchange`: после разрыва B–C recovery успешно строил C–A,
|
||||
нарушая предположение теста о фиксированной цепочке. Теперь A и C принимают
|
||||
только ключ B: все прежние проверки withdraw/restore сохранены, обход запрещён
|
||||
условиями тестовой сети. Автоматический обход отдельно проверяет `test_topo_recovery`.
|
||||
- `test_tcp_io`: адрес 192.0.2.1:81 в окружении устанавливал TCP-соединение,
|
||||
поэтому connect timeout закономерно не срабатывал. Теперь используется локальный
|
||||
listener с заполненной accept-очередью, проверяется ETIMEDOUT. Перед уничтожением
|
||||
uasync тест завершает отложенное освобождение TCP и проверяет баланс таймеров.
|
||||
|
||||
Диагностика тестов включается `UTUN_TEST_DEBUG=1`; обычный запуск не включает DEBUG.
|
||||
Остаточные DATA после удаления соединения могут быть undecryptable и сами по себе
|
||||
не означают ошибку транспорта.
|
||||
|
||||
## Исправлено: гонка регистрации приглашения
|
||||
|
||||
`test_chat_join_e2e`, happy path A != C, падал из-за отсутствия подтверждения KEY_REGISTER.
|
||||
Раньше `chat_invite_build_link()` вызывал `chat_join_register_key()` и сразу возвращал ссылку.
|
||||
Успех `etcp_route_send()` означает постановку в очередь. Он не подтверждает обработку
|
||||
ключа посредником C. Параллельное подключение J–C может завершиться раньше доставки A–C.
|
||||
|
||||
Доказательство: `/tmp/join-e2e-debug.log`, один ключ `90c4b081833450a0`:
|
||||
|
||||
- 01:39:02.437851 — A пишет `join-key registered`, фактически ключ только поставлен в отправку.
|
||||
- 01:39:02.446527 — C отвергает JOIN_INFO_REQ: ключ ещё неизвестен.
|
||||
- 01:39:02.449724 — C сохраняет этот ключ, но J уже получил терминальный ERROR.
|
||||
|
||||
Теперь C отвечает KEY_REGISTER_ACK только после сохранения ключа. A сопоставляет
|
||||
channel/target/key и выдаёт ссылку после успешного ACK и локального сохранения ключа.
|
||||
Отрицательный ACK завершает запрос ошибкой. Чужие, неподписанные, незашифрованные,
|
||||
повторные и запоздалые подтверждения не могут завершить другой запрос.
|
||||
|
||||
Контракт асинхронных API `chat_join_register_key` / `chat_invite_build_link`:
|
||||
NULL означает ошибку запуска без callback; принятый запрос даёт ровно один callback.
|
||||
Обычное завершение происходит после возврата API, включая self. Cancel/destroy
|
||||
завершают запрос синхронно с CANCELLED. Handle недействителен после callback.
|
||||
Для удалённого узла дедлайн 5 секунд; доставку и повтор потерянных пакетов обеспечивает
|
||||
надёжный router. Повторная регистрация того же ключа тем же инвайтером обновляет TTL;
|
||||
замена инвайтера для существующего ключа запрещена. Отмена может оставить ключ на C
|
||||
до истечения TTL, но A не авторизует по нему вход, поскольку локально такой ключ
|
||||
сохраняется только при успешном завершении.
|
||||
|
||||
GUI получает request_id в событии и игнорирует устаревшие результаты. Новый запрос
|
||||
отменяет предыдущее ожидание GUI. Headless связывает ответ с JSON id и отменяет все
|
||||
ожидания отключившегося клиента. Ни один из путей не запускает вложенный event loop.
|
||||
|
||||
Дополнительные исправления:
|
||||
|
||||
- Устранён double-free при ошибке `etcp_route_send`: пакет уже освобождён роутером.
|
||||
- Headless прекращает разбор пакета после `quit`; последующая команда не создаёт
|
||||
запрос на уже закрытом клиенте. При destroy отменяются cleanup_timer и запросы,
|
||||
удаляются клиентские сокеты и освобождается контекст.
|
||||
- UBSan обнаружил невыровненное чтение group_id из `d + 1` в chat_sync. Оно и три
|
||||
аналогичных диагностических чтения заменены на memcpy.
|
||||
- Тест chat_join теперь останавливает вручную запущенный member_sync;
|
||||
проверяется баланс выделения/освобождения таймеров.
|
||||
|
||||
Проверки:
|
||||
|
||||
- `test_chat_join`: 31/31, включая задержанный/отсутствующий ACK, таймаут,
|
||||
неверные channel/peer/key/flags/размер/status, дубликат, отказ хранения из-за
|
||||
конфликта инвайтера, отмену, shutdown, параллельные ключи и ошибку отправки.
|
||||
- `test_chat_join_e2e`: 3/3 (A≠C, A=C, неверный ключ). C принудительно откладывает
|
||||
регистрацию на 200 мс; проверяется отсутствие ссылки до обработки регистрации.
|
||||
Дополнительно проверяются подавление старого GUI-запроса и реальный headless
|
||||
control socket: параллельные ответы, валидность ссылок, quit и команда после него.
|
||||
- ASan/UBSan: оба теста проходят. LeakSanitizer и баланс таймеров проверены в
|
||||
test_chat_join; E2E использует fork/_exit, поэтому сам по себе не проверяет утечки
|
||||
при завершении дочерних процессов. Инструментированы изменённые chat-модули и
|
||||
transport/routing, остальные объекты взяты из обычной библиотеки.
|
||||
Логи: `/tmp/utun-ack-join-asan.log`, `/tmp/utun-ack-e2e-asan.log`.
|
||||
|
||||
## Отдельный root-тест auto_socket_dynamic
|
||||
|
||||
Запуск в `sudo unshare --net` с поднятым loopback закончился TIMEOUT через 30 секунд.
|
||||
Лог: `/tmp/utun-ack-root-checks.log`. Это не PASS основного набора: в нём тест пропущен.
|
||||
|
||||
В конфиге теста включён auto_sockets, но не отключён стандартный фильтр
|
||||
`auto_socket_skip_no_default_route=1`. Dummy-интерфейсы теста не имеют default route,
|
||||
а `auto_socket.c:reconcile_iface()` исключает такие интерфейсы. Лог показывает
|
||||
`no links created`, затем TIMEOUT. Исправленный тест создаёт собственный network
|
||||
namespace, поднимает loopback и добавляет default route только клиентским dummy.
|
||||
Фильтр остаётся включённым; отдельно проверяется исключение серверного dummy,
|
||||
иначе его сокет скрывал бы полный обрыв клиентской сети.
|
||||
|
||||
Кроме того, тест сохраняет handle общего дедлайна, traffic_send и traffic_monitor
|
||||
в одну переменную timeout_handle; фазовые таймеры вообще не сохраняются. При выходе
|
||||
не отменялись все таймеры; зафиксирован остаток 9 таймеров (665/656). В обработчиках
|
||||
трафика освобождался entry без queue_dgram_free. Теперь у каждого таймера свой slot,
|
||||
waiters отменяются до teardown, payload освобождается, deferred cleanup завершается
|
||||
до проверки баланса таймеров, сокетов и `u_get_allocated_count()`.
|
||||
|
||||
Новый генератор шлёт 1024-байтовые пакеты независимо в обе стороны, по одному через
|
||||
threshold waiter, с бюджетом 64 пакета на tick. Проверяются содержимое, строгий seq,
|
||||
ограниченность TX/RX очередей, реальное срабатывание backpressure, остановка
|
||||
генераторов при полном обрыве и полное опустошение после восстановления. NCD handle
|
||||
удерживает логическую сессию: смена интерфейса не является разрешением терять её данные.
|
||||
Остатки после действительно удалённого соединения — отдельный допустимый случай.
|
||||
|
||||
Прогон нагрузки обнаружил ошибки, скрытые прежними проверками:
|
||||
|
||||
- `tcp_socket_add` не сохранял `netif_index`: монитор принимал interface-bound сокет
|
||||
за сокет default route и менял его адрес при событиях другого интерфейса.
|
||||
- Удаление TCP модели искало её по имени уже удалённого интерфейса и порту из БД.
|
||||
Теперь запись интерфейса хранит имя и прямую ссылку на модель; один путь закрывает
|
||||
listener, линки и модель. Портовые записи удаляются с сохранённым именем.
|
||||
- `tcp_socket_remove` не закрывал линки, созданные вне NCD. Теперь закрывает все
|
||||
связанные линки до освобождения модели; root-тест создаёт такой линк отдельно.
|
||||
- Поиск UDP модели не исключал TCP. Добавлена проверка типа сокета.
|
||||
- При selective ACK sender мог выдавать новые seq за границей `MAX_INFLIGHT_SIZE`
|
||||
относительно cumulative ACK. Теперь генерация seq останавливается на границе,
|
||||
cumulative ACK возобновляет очередь даже при отсутствии уже selectively-acked записей.
|
||||
Тесты проверяют блокировку, возобновление и uint32 wrap. ACK не может подтвердить
|
||||
ещё не назначенный seq; декодирование старшего байта seq/ACK не делает signed shift.
|
||||
- Timestamp Q16 умножался как signed int и переполнялся на циклических/отрицательных
|
||||
смещениях. Перевод выполняется в uint32. Jitter раньше всегда получал нулевую разницу:
|
||||
предыдущий RTT читался после перезаписи; теперь берётся до неё, EWMA использует int64.
|
||||
- UBSan выявил невыровненные canary/указатели в `mem.c` и `memory_pool.c`. Метаданные
|
||||
читаются/пишутся через memcpy; покрыты размеры блоков 1..17 и повторное использование пула.
|
||||
- Повторный общий прогон поймал `test_etcp_connect`: PID-порты пересекались с занятым
|
||||
UDP-портом (`EADDRINUSE`). Теперь сервер bind-ится на порт 0, клиент получает фактический
|
||||
порт через getsockname; собственного алгоритма поиска свободного порта нет.
|
||||
|
||||
Root ASan/UBSan/LSan: PASS, 153335 / 175912 пакетов, reinit=0, все очереди пусты,
|
||||
баланс памяти/таймеров/сокетов совпал (`/tmp/root-fix-asan.log`). Инструментированы
|
||||
тест, ETCP/session/router/NCD/autosocket/STCP/normalizer, очереди, event loop и аллокаторы;
|
||||
прочие объекты архивов обычные. Unit-логи: `/tmp/root-session-asan.log`,
|
||||
`/tmp/root-memory-asan.log`. LSan запускается вне sandbox: под ptrace он не работает.
|
||||
|
||||
Финальная чистая обычная сборка: `./check.sh` — **97 PASS / 0 FAIL / 1 SKIP**;
|
||||
proxy, burst и load — PASS (`/tmp/root-fix-final-check.log`). Отдельный root-тест —
|
||||
PASS, 188069 / 191552 пакета, все очереди пусты, reinit=0, баланс ресурсов совпал
|
||||
(`/tmp/root-test-final.log`). После добавления drain отложенной очистки
|
||||
`test_etcp_connect` повторно прошёл без остаточных таймеров (`/tmp/root-connect-final.log`).
|
||||
|
||||
Подробные socket/connection INFO включаются только через `UTUN_TEST_DEBUG=1`.
|
||||
`ENETUNREACH` непосредственно при принудительном удалении IP/интерфейса ожидаем до
|
||||
обработки netlink; тест требует последующей доставки всех данных. Ошибки выхода
|
||||
за receive window недопустимы и после исправления sender gate не наблюдаются.
|
||||
|
||||
## Попутная проблема диагностики
|
||||
|
||||
`lib/debug_config.c:debug_parse_config()` требует `:`, хотя документация описывает `=`.
|
||||
Кроме того, функция всё ещё вычисляет индекс через битовую маску, хотя категории
|
||||
теперь являются индексами. Исправление в этот набор не включено; тесты используют
|
||||
`debug_set_category_level()` напрямую.
|
||||
+132
-39
@@ -41,6 +41,12 @@ inflight - две очереди (ll_queue):
|
||||
- send_hist[8] - история через какие линки отправлялся (индекс линка)
|
||||
- inflight_bytes/inflight_packets per-link отслеживается в ETCP_LINK
|
||||
Размер буфера inflight лимитируем как сумму по всем активным линкам optimal_inflight.
|
||||
Отдельно ограничиваем размах sequence относительно кумулятивного ACK: новый seq выдаётся
|
||||
только если (uint32_t)(next_tx_id - rx_ack_till) <= MAX_INFLIGHT_SIZE (16384).
|
||||
Selective ACK освобождает отдельную inflight-запись, но не продвигает эту границу:
|
||||
приёмник пока не может освободить пакеты за дыркой. Кумулятивный ACK возобновляет
|
||||
input_queue даже если подтверждённые записи уже удалены по selective ACK.
|
||||
ACK ограничивается последним назначенным seq (next_tx_id - 1); сравнения учитывают uint32 wrap.
|
||||
optimal_inflight расчитывается из RTT и bandwidth линка.
|
||||
|
||||
bandwidth по каждому линку адаптивно подстраиваем:
|
||||
@@ -59,8 +65,12 @@ Burst-измерения bandwidth (0x07, 0x08) зарезервированы,
|
||||
|
||||
шифруется строго ВСЁ включая ВСЕ заголовки (кроме обфусцированного publickey в INIT-пакетах).
|
||||
timestamp вставляется ВСЕГДА ВО ВСЕ КОДОГРАММЫ (скрыто в шифрованном заголовке).
|
||||
Итого шифруется: 3 байта заголовка + (data_len - noencrypt_len) байт данных.
|
||||
2. Опциональные секции (одна или несколько) добавляются в etcp.c (внутри data[], без доп. заголовка):
|
||||
Для DATA/ACK после этих 3 байт добавляется обязательный заголовок сессии:
|
||||
[0x09][sender_epoch(8 BE)][receiver_epoch(8 BE)], затем идут секции из etcp.c.
|
||||
Итого шифруется: 3 + 17 + data_len байт для DATA/ACK;
|
||||
для остальных кодограмм — 3 + (data_len - noencrypt_len) байт.
|
||||
Формат и подтверждение эпох описаны в разделе «Подтверждение транспортной сессии» ниже.
|
||||
2. Опциональные секции (одна или несколько) добавляются в etcp.c внутри DATA/ACK после заголовка сессии:
|
||||
а) Подтверждения (ACK) - заголовок 0x01:
|
||||
[0x01][count][last_delivered_id(4 байта LE)][rx_dup_count(2 байта LE)][[seq(4 LE)][recv_ts(2 LE)][dly(2 LE)] × count]
|
||||
- count: количество ACK записей (1 байт; до 32 ограничено размером пакета)
|
||||
@@ -108,82 +118,165 @@ Burst-измерения bandwidth (0x07, 0x08) зарезервированы,
|
||||
- клиент отправляет init запрос и выставляет таймаут
|
||||
- по таймауту повторяет init запросы с экспоненциально растущим интервалом (начальный 500ms, макс 50s, +25% каждые 10 попыток), ведется счетчик запросов init.
|
||||
- когда клиент получает init подтверждение — снимает таймаут, выставляет link_state=connected, запускает keepalive.
|
||||
- сервер при получении init запроса: если линк существует и session_id совпал — отвечает без сброса (0x05); если session_id новый — выполняет etcp_conn_reinit и отвечает со сбросом (0x03).
|
||||
При добавлении нового канала: если link_state=1 (handshake) — посылаем INIT со сбросом (0x02); если link_state=2 (reconnect) — без сброса (0x04).
|
||||
- каждая сторона сообщает в INIT собственный reset_id (64 бита, BE); ответ не повторяет эпоху запроса.
|
||||
- первая неизвестная эпоха пира сохраняется из handshake. Смена уже известной эпохи запускает challenge;
|
||||
только подходящий CONFIRM сбрасывает поток и заменяет эпоху пира, сохраняя собственную.
|
||||
- повтор INIT и коллизия встречных подключений сами по себе не сбрасывают поток.
|
||||
При добавлении нового канала: если link_state=1 (handshake) — посылаем INIT_REQUEST (0x02);
|
||||
если link_state=2 (reconnect) — INIT_REQUEST_NOINIT (0x04). Коды INIT не заменяют подтверждение смены эпохи.
|
||||
link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
|
||||
|
||||
**** Формат кодограмм для etcp_connections.c/h ****
|
||||
Кодограммы с этими секциями обрабатываются в etcp_connections (в этих кодограммах всегда одна секция-тип пакета). В обязательном заголовке (timestamp+flag_up) ID не используется.
|
||||
|
||||
Типы пакетов:
|
||||
0x02 — ETCP_INIT_REQUEST (со сбросом etcp сессии)
|
||||
0x03 — ETCP_INIT_RESPONSE (подтверждение со сбросом)
|
||||
0x04 — ETCP_INIT_REQUEST_NOINIT (без сброса)
|
||||
0x05 — ETCP_INIT_RESPONSE_NOINIT (подтверждение без сброса)
|
||||
0x02 — ETCP_INIT_REQUEST (начальный handshake линка)
|
||||
0x03 — ETCP_INIT_RESPONSE (вариант ответа INIT)
|
||||
0x04 — ETCP_INIT_REQUEST_NOINIT (восстановление линка, также разрешение коллизии)
|
||||
0x05 — ETCP_INIT_RESPONSE_NOINIT (вариант ответа INIT)
|
||||
0x06 — ETCP_PING (проверка доступности)
|
||||
0x07 — ETCP_PONG (ответ на пинг)
|
||||
0x08 — ETCP_KEEPALIVE
|
||||
0x09 — ETCP_SESSION_DATA (обёртка секций DATA/ACK с эпохами)
|
||||
0x0a — ETCP_SESSION_HELLO
|
||||
0x0b — ETCP_SESSION_CHALLENGE
|
||||
0x0c — ETCP_SESSION_CONFIRM
|
||||
0x0d — ETCP_TCP_ADDRESS (STCP: code + family(4/6) + observed IP(16), без порта)
|
||||
|
||||
Варианты ответа 0x03/0x05 не определяют смену эпохи: клиент в обоих случаях обрабатывает reset_id одинаково.
|
||||
|
||||
1) Init запрос (0x02 / 0x04):
|
||||
[code(1)] [my_node_id(8 BE)] [session_id(4 BE)] [mtu_local(2)] [keepalive_interval(2)]
|
||||
[recovery_interval/100(2)] [local_link_id(1)] [sock_id(1)] [only_local(1)] [type(1)]
|
||||
[code(1)] [node_id(8 BE)] [reset_id(8 BE)] [mtu(2 BE)] [keepalive(2 BE)]
|
||||
[recovery(2 BE)] [link_id(1)] [socket_id(1)] [only_local(1)] [type(1)]
|
||||
[src_ipv4(4)] [src_port(2 BE)] [collision(1)] [ed25519_pubkey(32)] [device_type(1)]
|
||||
[random_padding...] [salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
|
||||
- code: 0x02 (со сбросом) или 0x04 (без сброса)
|
||||
Фиксированное тело до padding — 67 байт (ETCP_INIT_REQUEST_PKT).
|
||||
- code: 0x02 или 0x04, вариант handshake линка
|
||||
- node_id: 64 бита, идентификатор узла (big-endian)
|
||||
- session_id: 32 бита, случайный ID сессии генерируется клиентом (big-endian)
|
||||
- mtu_local: uint16_t локальный MTU
|
||||
- keepalive_interval: uint16_t интервал keepalive в мс
|
||||
- recovery_interval: uint16_t = recovery_interval/100 (в таймбазах 0.1мс / 100)
|
||||
- local_link_id: uint8_t (0-255), назначается отправителем
|
||||
- sock_id: uint8_t, уникальный ID сокета отправителя
|
||||
- reset_id: uint64_t, собственная локальная эпоха потока клиента (big-endian)
|
||||
- mtu: uint16_t локальный MTU
|
||||
- keepalive: uint16_t интервал keepalive в мс
|
||||
- recovery: uint16_t = link->recovery_interval/100 (исходная единица — 0.1 мс)
|
||||
- link_id: uint8_t (0-255), назначается отправителем
|
||||
- socket_id: uint8_t, уникальный ID сокета отправителя
|
||||
- only_local: uint8_t, флаг "только локальные подключения"
|
||||
- type: uint8_t, тип сокета (CFG_SERVER_TYPE_UNKNOWN=0/PUBLIC=1/NAT=2/PRIVATE=3/LOCAL=4)
|
||||
- type: uint8_t, тип сокета (CFG_SERVER_TYPE_UNKNOWN=0/PUBLIC=1/NAT=2/PRIVATE=3/LOCAL=4)
|
||||
- src_ipv4/src_port: адрес интерфейса отправителя; для IPv6 src_ipv4=0, порт сохраняется;
|
||||
при SOCKS5 оба поля нулевые. Это заявленный адрес, а не наблюдение внешнего NAT-адреса.
|
||||
- collision: 1 — отправитель разрешает встречное подключение, заявляя роль master для линка
|
||||
- ed25519_pubkey: публичный ключ Ed25519 отправителя, 32 байта
|
||||
- device_type: CLIENT_TYPE_SERVER/DESKTOP/MOBILE
|
||||
- random_padding: случайные байты до размера handshake (защита от fingerprinting)
|
||||
- salt + obfuscated_pubkey: публичный ключ с обфускацией (не шифруется AES,
|
||||
но скрывается XOR с salt и собственным публичным ключом). Передаётся в конце пакета
|
||||
в noencrypt_len байтах (не попадает под AES-шифрование)
|
||||
Инициирует новый connection для tcp instance. Если tcp instance нет (первое подключение) - создаёт.
|
||||
Между нодами только одно подключение, но можно добавлять каналы.
|
||||
INIT устанавливает UDP-линк: создаёт или переиспользует общий ETCP_CONN к пиру.
|
||||
Между узлами одно ETCP-соединение с несколькими линками; TCP-линк использует STCP handshake.
|
||||
|
||||
2) Init подтверждение (0x03 / 0x05):
|
||||
[code(1)] [my_node_id(8 BE)] [session_id(4 BE)] [mtu_local(2)] [local_link_id(1)]
|
||||
[remote_socket_id(1)] [only_local(1)] [type(1)] [peer_ipv4(4)] [peer_port(2)]
|
||||
[random_padding...]
|
||||
- code: 0x03 (со сбросом) или 0x05 (без сброса)
|
||||
[code(1)] [node_id(8 BE)] [reset_id(8 BE)] [mtu(2 BE)] [link_id(1)]
|
||||
[remote_socket_id(1)] [only_local(1)] [type(1)] [peer_ipv4(4)] [peer_port(2 BE)]
|
||||
[ed25519_pubkey(32)] [device_type(1)] [keepalive(2 BE)] [random_padding...]
|
||||
Фиксированное тело до padding — 64 байта (ETCP_INIT_RESPONSE_PKT).
|
||||
- code: 0x03 или 0x05, вариант ответа INIT; сам по себе не сбрасывает поток клиента
|
||||
- node_id: 64 бита, big-endian
|
||||
- session_id: 32 бита, big-endian (должен совпасть с запросом клиента; если нет — клиент игнорирует)
|
||||
- mtu_local: uint16_t локальный MTU сервера
|
||||
- local_link_id: uint8_t идентификатор канала сервера
|
||||
- remote_socket_id: uint8_t (копируется из запроса)
|
||||
- reset_id: uint64_t, собственная локальная эпоха потока сервера (big-endian), не эхо запроса
|
||||
- mtu: uint16_t локальный MTU сервера
|
||||
- link_id: uint8_t идентификатор канала сервера
|
||||
- remote_socket_id: uint8_t (ID серверного сокета; ID клиентского сокета известен серверу из запроса)
|
||||
- only_local: uint8_t флаг сервера
|
||||
- type: uint8_t тип сокета сервера
|
||||
- peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер — для NAT traversal)
|
||||
- peer_port: 2 байта (порт клиента как его видит сервер)
|
||||
- peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер, в сетевом порядке)
|
||||
- peer_port: uint16_t (порт клиента как его видит сервер, big-endian)
|
||||
При IPv6 оба поля нулевые.
|
||||
- ed25519_pubkey: публичный ключ Ed25519 сервера, 32 байта
|
||||
- device_type: CLIENT_TYPE_SERVER/DESKTOP/MOBILE
|
||||
- keepalive: uint16_t интервал keepalive сервера в мс
|
||||
- random_padding: случайные байты
|
||||
|
||||
3) PING (0x06):
|
||||
[0x06] [node_id(8 BE)] [nonce(8 BE)] [user_data_len(2 BE)] [user_data...]
|
||||
[0x06] [flags(1)] [node_id(8 BE)] [nonce(8 BE)] [user_data_len(2 BE)] [user_data...] [padding...]
|
||||
[salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
|
||||
Отправляется с отдельным crypto-контекстом (не связан с ETCP-сессией).
|
||||
Используется для проверки доступности узла и NAT-тестирования.
|
||||
|
||||
4) PONG (0x07):
|
||||
[0x07] [node_id(8 BE)] [nonce(8 BE)] [resp_data_len(2 BE)] [response_data...]
|
||||
[0x07] [flags(1)] [node_id(8 BE)] [nonce(8 BE)] [resp_data_len(2 BE)] [response_data...] [padding...]
|
||||
[salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
|
||||
Ответ на PING. nonce копируется из запроса для сопоставления.
|
||||
Nonce генерируется CSPRNG. Проверяются также ключ отправителя, source IP:port,
|
||||
ID и поколение принимающего сокета. Флаг OBSERVE (0x04) запрашивает ответ
|
||||
address(19) || tag(25): family(4/6), IP(16), port(2, BE), NAT2UDP!(8),
|
||||
socket_id(1), generation(8, BE), round(8, BE).
|
||||
ADDRESS_CHECK (0x08) — аутентифицированное входящее испытание endpoint.
|
||||
Модель NAT и алгоритм владельца: doc/nat_detection.txt.
|
||||
|
||||
При получении init получатель пакета должен:
|
||||
- reset ETCP_LINK с этим ip_port если он есть
|
||||
- создать новый ETCP_CONN с этим node_id. если уже существует подключение с этим node_id - проверить ключи на совпадение.
|
||||
При получении INIT получатель создаёт или переиспользует ETCP_CONN и подходящий линк;
|
||||
для существующего соединения проверяет совпадение ключа пира. Эпоха из INIT учитывается
|
||||
через etcp_conn_apply_peer_reset_id, без немедленного сброса потока при смене известной эпохи.
|
||||
приём кодограмм из UDP выглядит так:
|
||||
uasync select -> etcp_connection.c etcp_connections_read_callback: memory_pool_alloc, decrypt (or init) -> etcp_conn_input (etcp.c/h - сам tcp механизм) -> ETCP_CONN output_queue
|
||||
|
||||
Keepalive: пакет без секций (только timestamp+flag_up в шифрованном заголовке, data_len=0).
|
||||
Keepalive: [0x08] [flags(1)] [period_ms(2 LE)] [sleep_fields?:4] [padding...].
|
||||
При KA_FLAG_SLEEP добавляются active_remaining(2 LE) и sleep_duration(2 LE), оба в единицах 0.1 с.
|
||||
Полезное тело занимает 4 байта без SLEEP или 8 байт с SLEEP; остальные байты — padding.
|
||||
keepalive пакеты шлют и клиент и сервер с заданным интервалом если нет полезного трафика.
|
||||
Сервер прекращает слать keepalive если линк потерян (ждёт keepalive от клиента).
|
||||
flag_up в заголовке устанавливается в значение recv_keepalive (признак что локальная сторона принимает пакеты).
|
||||
|
||||
Padding keepalive, PING/PONG и STCP-handshake:
|
||||
- ETCP_CONN хранит собственный диапазон min/max и независимый таймер для всех своих линков.
|
||||
- UTUN_INSTANCE хранит отдельный диапазон и таймер для PING/PONG без существующего ETCP_CONN.
|
||||
- При создании владельца и каждом обновлении: min=rand(0,30), max=min+rand(32,128), границы включительные.
|
||||
- Следующее обновление назначается через rand(50,300) секунд. При закрытии владельца таймер отменяется.
|
||||
- Каждый пакет независимо выбирает длину rand(min,max); padding заполняется нулями ДО шифрования.
|
||||
- UDP PING/PONG: padding шифруется перед CRC/tag; открытый salt+pubkey остаётся в конце датаграммы.
|
||||
user_data_len/resp_data_len обозначают только полезные данные. PONG не копирует padding запроса.
|
||||
- STCP: salt(8)+pubkey(32) открыты, затем 51 байт зашифрованного handshake и padding_size байт
|
||||
нулевого padding, зашифрованного продолжением того же CTR. Приём продвигает CTR через padding;
|
||||
padding_size=0 сразу завершает handshake. Параметры берутся у ETCP_CONN, иначе у instance.
|
||||
- Каждый STCP-поток использует соль своего отправителя: клиентскую для client→server, серверную для server→client.
|
||||
CTR IV = первые 12 байт SHA256(session_key || stream_id_LE || salt(8) || "uTun3-stream") + четыре нулевых байта.
|
||||
Отправка инициализируется после генерации своей соли, приём — после чтения соли пира, до расшифровки тела.
|
||||
После handshake CTR непрерывно продолжается через padding и DATA, без повторной инициализации на кадр.
|
||||
- Padding не должен превышать 158 байт. Для UDP длина ограничивается оставшимся MTU;
|
||||
если не помещается даже min, отправка завершается ошибкой без обрезания пользовательских данных.
|
||||
- Диагностика диапазонов/обновлений/PING/PONG: etcp=debug; keepalive: keepalive=debug.
|
||||
|
||||
Клиент: если все линки =down то начинается процедура восстановления связи:
|
||||
- клиент посылает init без сброса (0x04), link_state=2
|
||||
- сервер как получает init проверяет session_id:
|
||||
- если session_id совпадает — шлёт подтверждение без сброса (0x05)
|
||||
- если session_id изменился (клиент перезапустился) — шлёт подтверждение со сбросом (0x03)
|
||||
- сервер восстанавливает физический линк и отвечает INIT_RESPONSE/INIT_RESPONSE_NOINIT со своей эпохой.
|
||||
- прежняя эпоха пира не меняет поток; смена известной эпохи подтверждается через CHALLENGE/CONFIRM.
|
||||
Код ответа 0x03/0x05 не является подтверждением смены эпохи.
|
||||
|
||||
**** Подтверждение транспортной сессии (etcp_session.c/h) ****
|
||||
Physical UDP INIT / STCP handshake establishes an authenticated link. A stream reset
|
||||
uses the same control protocol on every initialized UDP or TCP link, bypassing the
|
||||
normalizer and reliable DATA/ACK queues. A stream reset does not generate physical DOWN/UP.
|
||||
|
||||
Encrypted DATA/ACK plaintext after timestamp(2) + flag_up(1):
|
||||
09 | sender_epoch:u64be | receiver_epoch:u64be | existing ACK/DATA sections
|
||||
The extra 17 bytes participate in the MTU budget. Unwrapped DATA/ACK and frames with
|
||||
an epoch mismatch are discarded before stream state or link liveness is updated.
|
||||
|
||||
Control frames (all 25 bytes after timestamp/flag):
|
||||
0a HELLO | sender_epoch | receiver_epoch | 0
|
||||
0b CHALLENGE | sender_epoch | receiver_epoch | random_cookie:u64be
|
||||
0c CONFIRM | sender_epoch | receiver_epoch | echoed_cookie:u64be
|
||||
|
||||
Each connection owns one pending peer epoch, fresh challenge, and retry timer.
|
||||
The first nonzero peer epoch from a physical handshake is stored directly if unknown.
|
||||
HELLO and a change to an already known epoch advertised by INIT only request a challenge. Only a
|
||||
CONFIRM for the current local epoch and pending random challenge commits a peer
|
||||
epoch. Changing the peer epoch resets the stream but preserves our own epoch.
|
||||
A local fatal reset generates a new local epoch and blocks DATA until confirmation.
|
||||
Both sides may reset concurrently. Loss of any control frame is recovered by a
|
||||
500 ms retry on all initialized links. After 10 s an unconfirmed local reset closes
|
||||
the connection; an unconfirmed candidate alone is discarded without closing a
|
||||
working session. Close cancels the timer before destroying links.
|
||||
|
||||
TCP may initially receive peer epoch zero because the server creates ETCP_CONN
|
||||
only after STCP handshake. It completes the same session confirmation before INIT/UP.
|
||||
REINIT notifies stream consumers without tearing down physical ownership. The
|
||||
router retains end-to-end inflight; BGP repeats JOIN_GROUP and TABLE_REQUEST because
|
||||
its previous control messages may have been discarded by the stream reset.
|
||||
@@ -1,3 +1,5 @@
|
||||
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](../src/routing_layer/etcp_router_doc.md).
|
||||
|
||||
# ETCP Router Architecture
|
||||
|
||||
## Структуры данных
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
Аватарка CHAT-группы
|
||||
===================
|
||||
|
||||
Модуль src/chat/group_avatar.c. Картинка — бинарный BLOB до 16384 байт в общей
|
||||
SQLite каждого участника: channels.avatar_data/version/hash/signature.
|
||||
Storage-сервер, каталоги файлов и пользовательские квоты не используются.
|
||||
При старте добавляются отсутствующие колонки одной транзакцией, записи/ключи
|
||||
каналов сохраняются. channel_put использует UPSERT и сохраняет аватарку,
|
||||
UI-состояние и локальные приватные ключи; смена публичных ключей существующего
|
||||
channel_id запрещена. Удаление группы удаляет и её аватарку.
|
||||
|
||||
Право публикации — приватный Ed25519 ключ группы плюс собственное действующее
|
||||
членство. creator UID/admin-флаг не заменяют ключ. group_avatar_set подписывает:
|
||||
ASCII "group-avatar-v1" без NUL, group:u64 LE, version:u64 LE, size:u16 LE, bytes.
|
||||
Версия — max(NTP microseconds/1000, previous+1), ограничена INT64_MAX.
|
||||
SHA256 считается по картинке; при одинаковой версии побеждает больший SHA256.
|
||||
Обе конкурирующие записи должны иметь правильную подпись группы; конфликт — WARN.
|
||||
Публикация одинаковой картинки не создаёт новую версию. Удаление — подписанная
|
||||
запись с size=0 и новой версией; подпись/версия сохраняются, старый пир не может
|
||||
восстановить удалённую картинку. Чтение проверяет сохранённый хеш и подпись.
|
||||
|
||||
Транспорт
|
||||
---------
|
||||
|
||||
ETCP_RT_ID_GROUP_AVATAR=0x3A, router wire version=1. Отправитель/группа берутся
|
||||
из проверенного router-заголовка. Все пакеты подписаны ключом узла и зашифрованы
|
||||
end-to-end; в DATA дополнительно передаётся подпись группы над всей картинкой.
|
||||
Обмен только между READY-соседями CHAT-группы с действующим подписанным членством.
|
||||
Любой участник пересылает сохранённую запись, владелец ключа может быть offline.
|
||||
|
||||
Все числа протокола LE. Общая часть, 44 байта:
|
||||
version:u8=1, command:u8, avatar_version:u64, total_size:u16, SHA256:32.
|
||||
STATE=1 объявляет локальную версию/хеш; начальное отсутствие — version=0, hash=0.
|
||||
GET=2 запрашивает актуальную запись. Объявление не меняет БД без полной DATA.
|
||||
DATA=3 добавляет group_signature:64, offset:u16, block_size:u16, bytes.
|
||||
DATA header=112 байт. Картинка разбивается на блоки по 4096 байт; последний
|
||||
короче. Полная картинка 16 КиБ — ровно четыре DATA. Удаление — один DATA без bytes.
|
||||
|
||||
Порядок блоков гарантирует router. Сборка проверяет offset, постоянные version,
|
||||
total_size/hash/signature и допустимые размеры. До полной сборки прежняя картинка
|
||||
остаётся в БД. После SHA256/Ed25519 вся запись заменяется одним UPDATE, затем
|
||||
CHANNEL_UPDATED доставляет изменение GUI и STATE распространяется соседям.
|
||||
Промежуточные блоки не публикуются, ACK/retransmit каждого блока — только router.
|
||||
|
||||
Отправка заполняет router до нижнего порога send_q count<=0 и ждёт send_ready;
|
||||
планировщик отправляет максимум четыре пакета за callback. Snapshot отправителя
|
||||
не меняется при новом локальном обновлении. Максимум 16 tx и отдельно 16 rx
|
||||
сборок на экземпляр, каждая до 16 КиБ. receive_ready при заполнении rx сохраняет
|
||||
пакет в router recv_q без продвижения ACK; освобождение слота возобновляет приём.
|
||||
Несовпадение эпох/закрытие router отменяет незавершённую сборку и начинает сверку.
|
||||
Эпоха нового контекста берётся из текущей READY-сессии, не сбрасывает первый приём.
|
||||
Обнаружение соседей/таймауты — таймер 1 с; STATE также раз в 30 с. Нет прогресса
|
||||
15 с — отмена незавершённого snapshot, повтор запроса/сверки. Остановка сервиса
|
||||
отменяет таймеры/waiters/call_soon до удаления группы/ядра.
|
||||
|
||||
ChatGUI
|
||||
-------
|
||||
|
||||
Настройки группы: Upload group avatar/Remove group avatar, предпросмотр и статус.
|
||||
Кнопки доступны только при локальном приватном ключе. Применение сразу, отдельно
|
||||
от Save локальных настроек. Worker обрезает центральный квадрат, создаёт JPEG
|
||||
128×128, снижает качество/размер до лимита 16 КиБ. Вход ограничен 16 мегапикселями.
|
||||
GUI отправляет байты в uasync; ядро повторно проверяет размер/членство/ключ.
|
||||
|
||||
SQLite — постоянный кеш. GroupAvatarManager хранит QImage до 32 МиБ, ключ кеша:
|
||||
group/version/hash. Декодирование в отдельном worker, проверка SHA256, JPEG и
|
||||
квадратных размеров до 128×128. Поздний результат старой версии не заменяет
|
||||
новую. Готовая картинка обновляет только строку группы; без аватарки — инициалы.
|
||||
Приватные ключи/байты картинки не выводятся в диагностику.
|
||||
|
||||
Проверки
|
||||
--------
|
||||
|
||||
test_group_avatar: настоящие два ядра/UDP/router, 16 КиБ четырьмя блоками,
|
||||
атомарность сборки, hash/signature/cross-group отказ, приостановка receive,
|
||||
перезапуск канала, stale/conflict, редактор с ключом вне creator UID, подписанное
|
||||
удаление, повторное описание группы и restart БД. Schema failure откатывает
|
||||
частичные ALTER, сохраняет прежние данные/ключи, повторное добавление идемпотентно.
|
||||
test_group_avatars (Qt): нормализация, лимит/отображение, кеш, права кнопок и отказ
|
||||
ядра без ключа, удаление. chat=debug показывает состояния и каждый 4КиБ блок;
|
||||
chat=info — публикации и жизненный цикл. Ошибки содержат group/peer/version/offset.
|
||||
@@ -0,0 +1,528 @@
|
||||
# Linux: диагностика и восстановление аудио
|
||||
|
||||
Проверка 2026-10-02: desktop chatgui, miniaudio/PulseAudio, общий голосовой стек,
|
||||
звонки, рация и запись сообщений. Исправления обработки PCM, владения устройствами
|
||||
и автоматическое восстановление реализованы. Ниже сохранены исходные результаты
|
||||
аудита; раздел «Реализация» описывает действующие контракты и проверки.
|
||||
|
||||
## Вывод
|
||||
|
||||
До исправлений тракт был некорректен при сбоях. Самое существенное: воспроизведение
|
||||
звонка зависит от наличия микрофонных кадров в duplex-ring. Его опустошение
|
||||
останавливает вызов пользовательского callback, оставляя хвост выходного буфера
|
||||
незаполненным. Восстановление jitter-буфера не исправляет остановившееся устройство.
|
||||
Отдельного управляющего механизма восстановления устройств не было.
|
||||
|
||||
В реализации восстановлены три гарантии: каждый запрос output полностью заполнен; обработка
|
||||
одного callback всегда ограничена; остановка/пересоздание устройства выполняются
|
||||
вне аудиопотока и не уничтожают сетевую сессию звонка.
|
||||
|
||||
## Что установлено по работающим клиентам
|
||||
|
||||
В локальном старом логе звонка `132e4a9698f47bdf` с Samsung SM_A525F наблюдались
|
||||
паузы callback примерно 0,5–2,6 секунды при зарегистрированной обработке 0–2 мс.
|
||||
Входящий буфер desktop достигал 15 секунд, росли потери, включалось ускорение 160%.
|
||||
На Samsung захват продолжал давать около 50 кадров/с, playback регистрировал
|
||||
underrun и тоны разрыва. RTT порядка 1–4 мс не объясняет эти паузы сам по себе.
|
||||
|
||||
Это признаки проблемы обслуживания локального аудио, но ещё не доказательство
|
||||
конкретной причины зависания PulseAudio. Старое измерение исключало время
|
||||
синхронных DEBUG-вызовов между концом обработки и обновлением timestamp.
|
||||
Новая диагностика охватывает весь callback, включая эти логи.
|
||||
Работающий GUI не перезапускался, поэтому новый инструмент ещё не записал этот
|
||||
сбой на реальном звонке. Лог Samsung сохранён в `/tmp/sm-audio-current.log`.
|
||||
|
||||
## Найденные дефекты и ограничения до исправлений
|
||||
|
||||
| Место | Поведение и последствия | Необходимая доработка |
|
||||
| --- | --- | --- |
|
||||
| `lib/miniaudio.h`, `ma_device__handle_duplex_callback_playback()` | При пустом capture-ring `acquire_read` возвращает success с нулём кадров. Выполняется `break`, output остаётся частично незаполненным, call callback не вызывается. PulseAudio затем отправляет весь mapped buffer. | Обеспечить полный output и продолжение playback при отсутствии capture. Нельзя делать commit_read для синтетических кадров. |
|
||||
| `ma_device_on_write__pulse()` | Успешная запись с нулём обработанных кадров не уменьшает остаток цикла. Возможно бесконечное вращение с блокированием того же PA mainloop, включая capture. | Немедленно выходить при отсутствии продвижения, возвращать управление mainloop. Логировать состояние и запрос, не пытаться записывать до успеха в этом callback. |
|
||||
| `ma_device_on_read__pulse()` | PA hole отбрасывается без подачи соответствующего количества нулевых кадров. Из capture timeline исчезает интервал, duplex-ring может опустеть. | Сохранять длительность hole тишиной либо явно обозначать discontinuity независимому capture-тракту. |
|
||||
| `lib/speex_aec.c`, `speex_aec_feed_playback()` | Обрабатывает только остаток одного 960-sample кадра, отбрасывая хвост входного блока. Контракт разрешает произвольный count. | Циклом потреблять весь count, складывать все полные кадры, сохранять остаток. |
|
||||
| `CallAudioEngine::setPlaybackDeviceIndex()` | После неудачного пересоздания `m_active` остаётся true при отсутствии устройства; `start` того же звонка возвращает успех. | Разделить желание продолжать звонок и работоспособность устройства. Ошибка запуска переводит устройство в recovering. |
|
||||
| `RadioAudioEngine::teardownDevice()` | Если playback init успешен, а start нет, uninit пропускается: он зависит от флага started. Объект удаляется вместе с незавершёнными ресурсами. | Разделить initialized и started; каждый успешный init обязательно завершать uninit. |
|
||||
| `MainWindow::~MainWindow()` / `SoundManager::shutdown()` | Общий context освобождается вместе с engine, но звонок и рация здесь явно не останавливаются. Их singleton-объекты могут оставлять живые устройства. `m_context` также не обнуляется. | Сначала остановить все зависимые устройства и дождаться callbacks, затем освобождать общий context. |
|
||||
| `AudioRecorder` | Callback выбирает глобальный `g_recorder`, игнорируя `pUserData`. Второй recorder, в том числе тест микрофона, заменяет получателя. Флаги/уровень/вектор waveform используются из GUI и аудиопотока без согласованной синхронизации. | Использовать `pUserData`; передавать уровни атомарно, waveform — безопасным snapshot; буфер записи должен иметь однозначного владельца. |
|
||||
|
||||
До исправлений первые два дефекта воспроизводились стендом без аппаратуры:
|
||||
duplex оставлял sentinel в output при starvation, а PA-write без продвижения
|
||||
требовал внешней остановки. AEC из 1200 + 720 samples сохранял один кадр вместо
|
||||
двух. Теперь эти проверки требуют полного output, самостоятельного возврата
|
||||
из callback и двух сохранённых AEC-кадров; результаты приведены ниже.
|
||||
|
||||
Capture-AEC также обрабатывает не более одного кадра за вызов. В звонке его
|
||||
вызывают ровно с 960 samples, поэтому текущий call feed этот случай не нарушает.
|
||||
При расширении API нельзя объявлять произвольный count без согласованного размера
|
||||
выходного буфера. `speex_aec_reset()` уже существует: новый API для сброса самого
|
||||
AEC не нужен, но нужен согласованный сброс всего локального voice-тракта.
|
||||
|
||||
## Чистота реализации
|
||||
|
||||
Этот раздел фиксирует исходный аудит; изменения владения и ограничение работы
|
||||
callback описаны в разделе «Реализация».
|
||||
|
||||
Нормальная структура уже есть: GUI владеет устройствами, сервисный слой кодирует
|
||||
Opus и обрабатывает jitter, сетевой поток доставляет пакеты. Нет необходимости
|
||||
переписывать весь стек или заводить новый backend.
|
||||
|
||||
Однако границы владения не завершены: context фактически принадлежит engine
|
||||
звуковых уведомлений, а используется независимыми устройствами звонка/рации.
|
||||
Флаги active/started/initialized местами смешивают три разных состояния.
|
||||
Выбор input применяется к recorder, звонок использует default input, рация —
|
||||
default устройства. Output звонка и SoundManager тоже выбираются отдельно.
|
||||
Индекс перечисления не является устойчивой идентичностью устройства после hotplug.
|
||||
В radio capture fallback `useDefault` фактически повторяет ту же попытку:
|
||||
`pDeviceID` и так NULL в обеих ветках. Callbacks рации запускаются раньше
|
||||
подготовки capture-буфера и установки VAD; порядок запуска нужно привести к
|
||||
«сначала состояние и буферы, затем устройства», чтобы исключить гонки при старте.
|
||||
|
||||
Callback не является строго realtime-safe. Capture удерживает `g_mtx` во время
|
||||
AEC/AGC/Opus; отправка выделяет память и вызывает `uasync_post`. Playback вызывает
|
||||
SoundTouch и код с возможным расширением контейнеров, mutex и синхронными логами.
|
||||
Приём пакетов тоже может держать mutex при работе с jitter и логировании.
|
||||
Плохие Opus-кадры могут последовательно выбираться из большой очереди в одном
|
||||
pull: нужен предел работы на callback. Новая диагностика покажет, где это
|
||||
действительно нарушает deadline. Перенос всего DSP в ещё один worker заранее
|
||||
увеличит число очередей и точек синхронизации без установленной необходимости.
|
||||
|
||||
AEC получает reference только звонка. Звуки отдельного SoundManager в него не
|
||||
попадают; delay по умолчанию фиксирован, не вычисляется по фактической задержке
|
||||
устройств. Это ограничение качества эхоподавления, а не установленная причина
|
||||
текущего зависания.
|
||||
|
||||
Jitter умеет повторно накопить резерв после сетевого underrun, но диапазон tempo
|
||||
ограничен 1,0–1,6. Ускорение убирает избыток, а замедления для компенсации более
|
||||
медленных часов отправителя нет. При устойчивом отрицательном дрейфе резерв будет
|
||||
снова опустошаться. Это отдельная задача длительных звонков: после восстановления
|
||||
устройств проверить наклон глубины буфера и при необходимости добавить небольшую
|
||||
компенсацию дрейфа в обе стороны, не смешивая её с быстрым catch-up после паузы.
|
||||
|
||||
Принудительные 480 frames задают период 10 мс при 48 кГц. Собственный PA default
|
||||
miniaudio — 25 мс с комментарием о проблемах PipeWire при периодах меньше ~20 мс.
|
||||
Период backend и Opus-кадр 20 мс должны оставаться независимыми. Увеличивать
|
||||
период до исправления AEC нельзя: 1200-sample блоки уже воспроизводят потерю хвоста.
|
||||
После исправления разумно сначала проверить default backend и измерить задержку.
|
||||
|
||||
## Предлагаемое восстановление без большого усложнения
|
||||
|
||||
### 1. Сначала восстановить инварианты обработки и владения
|
||||
|
||||
Исправить полный output, zero-progress, AEC accumulation и uninit при неудачном
|
||||
start. Перед освобождением context явно остановить recorder, звонок, рацию и
|
||||
прочие его устройства. До остановки отменять recovery timer; после join можно
|
||||
сбрасывать voice-ресурсы. Проверять возвраты feed/start/init и логировать причину.
|
||||
|
||||
Самый небольшой вариант — оставить один duplex-device: при нехватке capture
|
||||
подать в call callback нулевой input, полностью сформировать playback и не
|
||||
продвигать реальный capture-ring за синтетические samples. Это чинит непосредственный
|
||||
дефект, но сохраняет общий жизненный цикл input/output и синтетический TX по
|
||||
playback-clock.
|
||||
|
||||
Для устойчивости к исчезновению микрофона предпочтительнее два устройства
|
||||
capture/playback внутри существующего `CallAudioEngine`, как уже сделано в рации.
|
||||
Capture только накапливает 960 samples и вызывает feed; playback независимо
|
||||
вызывает pull и всегда заполняет output. Это два handle с одним владельцем,
|
||||
без нового DSP-worker или дополнительной PCM-очереди. Потребуется уточнить
|
||||
потоковый контракт voice-слоя: один capture-writer и один playback-writer,
|
||||
согласованный доступ к AEC, остановка обоих до release. Текущий `g_mtx` уже
|
||||
защищает AEC, но остальные общие поля нужно проверить перед разделением.
|
||||
|
||||
### 2. Разделить краткий underrun и неисправность устройства
|
||||
|
||||
| Событие | Действие |
|
||||
| --- | --- |
|
||||
| Нет сетевого PCM, callback продолжает приходить | Тишина/существующий gap tone; jitter автоматически накапливает данные. Устройство не пересоздавать. |
|
||||
| Один backend underrun, новые кадры принимаются | Подать следующий полностью заполненный буфер; собрать счётчик. Не сбрасывать AEC и не запускать restart на каждый xrun. |
|
||||
| Hole/переполнение capture | Учесть потерянную длительность; краткую потерю пережить, длительную отметить discontinuity для AEC. |
|
||||
| Поток failed/stopped или нет продвижения кадров длительное время | Перейти в recovering и пересоздать затронутое устройство вне callback. |
|
||||
| Suspend/resume ОС | Учитывать suspend как отдельную причину, восстановление проверять после resume; не запускать частый restart-loop во время сна. |
|
||||
| Потерян общий backend/context | Однократно остановить всех его пользователей, восстановить владельца, затем требуемые устройства. |
|
||||
|
||||
Один owner timer и состояния `Stopped / Running / Recovering` достаточны.
|
||||
`wanted` хранит желание пользователя продолжать; generation меняется при stop,
|
||||
новом звонке и смене устройства. Каждая отложенная попытка проверяет generation
|
||||
и wanted — законченный звонок никогда не возрождается.
|
||||
|
||||
Callbacks только обновляют атомарные счётчики успешно обработанных кадров и
|
||||
признак backend failure. Watchdog проверяет фактический прогресс отдельно по
|
||||
направлениям. Тишина в PCM не означает остановку устройства. Reporter диагностической
|
||||
очереди не должен становиться watchdog: его события могут теряться.
|
||||
|
||||
Начальные значения для проверки: timer 200 мс, подозрение при отсутствии прогресса
|
||||
500 мс после startup grace; повторные попытки через 250/500/1000 мс с потолком
|
||||
2 секунды. Это предлагаемые настройки, не доказанные универсальные пределы.
|
||||
Suspend и фактический согласованный backend period должны учитываться отдельно.
|
||||
Нельзя обещать bounded recovery, пока init/stop/uninit сами могут зависнуть:
|
||||
сначала устранить spin-loop и проверить время backend-операций. Если они реально
|
||||
блокируют GUI, вынести только управление устройствами в один owner-thread,
|
||||
сохранив тот же автомат состояний.
|
||||
|
||||
На пересоздании сбросить capture-accumulator, AEC delay/filter и локальный PCM/
|
||||
stretch, сохранив call ID и сетевую сессию. После длительной аппаратной паузы
|
||||
устаревшую речь нужно явно отбросить до актуального резерва jitter, иначе
|
||||
15 секунд накопленного звука при скорости 160% разгружаются примерно 25 секунд.
|
||||
Этот сброс применять только при аппаратной discontinuity; обычный сетевой jitter
|
||||
не должен автоматически приводить к отбрасыванию очереди.
|
||||
|
||||
Устройство хранить по backend ID либо как default, не по индексу. При его
|
||||
исчезновении попробовать default и явно сообщить о смене; выбранное устройство
|
||||
можно вернуть при появлении. Нужны ошибки с ID/именем/направлением и результатом
|
||||
попытки. Для PA-ошибок следующий уровень диагностики — stream/context state и
|
||||
`pa_context_errno`, потому что один `-1` от API не объясняет причину.
|
||||
|
||||
### 3. Проверить сценарии, а не только успешный старт
|
||||
|
||||
После реализации добавить проверки: capture starvation 20/500 мс и возврат;
|
||||
hole; AEC блоки, пересекающие границу кадра; zero writable; ошибка записи;
|
||||
исчезновение выбранного устройства и появление default; suspend/resume;
|
||||
перезапуск звукового сервера; stop во время backoff; закрытие GUI при активном
|
||||
звонке/рации. Проверять полный output, bounded очереди и работу callback,
|
||||
возобновление прогресса, отсутствие callbacks после release и отсутствие
|
||||
повторного старта после пользовательского stop.
|
||||
|
||||
## Как читать добавленную диагностику
|
||||
|
||||
В GUI-категориях включить `call=debug` либо задать отдельную строку в INI:
|
||||
|
||||
```ini
|
||||
[debug]
|
||||
call=debug
|
||||
```
|
||||
|
||||
Обычные сводки идут на DEBUG, аномалии — WARN/ERROR; miniaudio info перенесён
|
||||
на DEBUG, подробный backend log — TRACE. Все устройства используют категорию
|
||||
call, включая recorder, radio и sound-engine; role и ID различают их в строках.
|
||||
Не включать глобальный trace для всего проекта при измерении аудио.
|
||||
|
||||
Reporter раз в секунду печатает stage, tid, callbacks/s, requested frames/s,
|
||||
максимальный интервал между входами, полное wall-time обработки и thread CPU-time,
|
||||
незавершённый вход и время с последнего входа. `work_max` и `cpu_max` — отдельные
|
||||
максимумы за окно, они не обязательно относятся к одному вызову.
|
||||
|
||||
| Stage | Что измеряется |
|
||||
| --- | --- |
|
||||
| loop | Один `pa_mainloop_iterate`, включая ожидание и вложенные callbacks. Долгое время само по себе не доказывает тяжёлый DSP. |
|
||||
| read / write | Полный backend read/write callback. Frames/s здесь — запрошенные, не гарантированно обработанные кадры. |
|
||||
| map | `writable_size` и `begin_write`; пустой/ошибочный выход тоже закрывает измерение. |
|
||||
| submit | Вызов `pa_stream_write`. |
|
||||
| duplex | Перенос capture-ring в duplex-output; detail содержит глубину capture-ring. |
|
||||
| callback | Полный miniaudio data callback, включая код приложения и его синхронные логи. |
|
||||
| feed / pull | Capture DSP/encode/send и playback jitter/stretch/tone в звонке. |
|
||||
|
||||
`unfilled_frames` — незаполненный output, `hole_frames` — потеря capture timeline,
|
||||
`capture_drop_frames` — переполнение capture-ring, `no_progress` — PA-write без
|
||||
продвижения. `backend_underruns` — отдельный callback самого PA-stream; это не
|
||||
то же самое, что пустой сетевой jitter. `backend playback started/resumed`
|
||||
показывает фактическое начало/возобновление playback. Notification содержит
|
||||
числовые miniaudio type/state.
|
||||
|
||||
Первое событие каждого типа за окно имеет tid и CLOCK_MONOTONIC timestamp;
|
||||
повторения суммируются. Для задержавшихся потоков reporter читает `/proc` wchan
|
||||
и накопленные schedstat CPU/ready-wait/slices. Сравнивать соседние snapshots:
|
||||
сами totals не являются длительностью текущей паузы. CPU-time намного меньше
|
||||
wall-time означает ожидание/вытеснение; wchan помогает отличить poll от mutex/
|
||||
I/O wait. Это снимок в момент отчёта, не исторический стек зависшего вызова.
|
||||
|
||||
Producer не выделяет память, не ждёт mutex reporter и не пишет диагностические
|
||||
логи: ограниченная MPSC-очередь 8192 события, не более четырёх CAS-попыток.
|
||||
Reporter не разыменовывает ma_device/pa_stream. Переполнение логируется как
|
||||
`diagnostic events lost`; такое окно нельзя считать полным. Инструментация
|
||||
добавляет получение часов/TID и атомарные операции, поэтому overhead ненулевой.
|
||||
Частые события call jitter/AEC/TX теперь публикуются счётчиками и выводятся из
|
||||
uasync вне аудиомьютекса. Часть существующих логов рации остаётся синхронной;
|
||||
тракт не объявляется строго realtime-safe.
|
||||
После close устройство удаляется из reporter: для устройств короче секунды
|
||||
доступны lifecycle и первые аномалии, но не полная секундная сводка.
|
||||
|
||||
Новый binary собран в `tools/chatgui/build/vibechat`; для нового измерения нужен
|
||||
его обычный перезапуск. Изменение файла binary не обновляет уже запущенный процесс.
|
||||
|
||||
## Реализация (2026-10-02)
|
||||
|
||||
`SoundManager` владеет общим контекстом и конечными выходами. На каждый фактический
|
||||
выход приходится один микшер: PCM сообщений/видео, заранее декодированные сигналы,
|
||||
готовый голос звонка и рации. После суммирования float PCM ограничивается, и один
|
||||
и тот же stereo буфер отправляется устройству и AEC соответствующего микрофона.
|
||||
Звук другого выхода в референс не входит. Default/явный backend ID объединяются;
|
||||
PulseAudio также публикует идентификатор фактического маршрута. После server move
|
||||
совпавшие выходы объединяются в GUI только после подтверждения фактического
|
||||
маршрута; предположение о default не используется вместо PA snapshot. При временной коллизии дополнительный выход
|
||||
отдаёт тишину до объединения. Изменение списка источников выполняется после stop
|
||||
callback, с сохранением backend stream и выбранного сервером маршрута.
|
||||
|
||||
Устройства сохраняются как backend ID вместо индекса. При init/start failure
|
||||
явного выбора пробуется default, причина логируется. Выбранный ID сохраняется;
|
||||
вернуться к нему можно повторным выбором или при следующем recovery. Автоматический
|
||||
опрос hotplug для возврата на первоначальный выбор не добавлен.
|
||||
|
||||
`AudioDevice` — владелец одного miniaudio device в GUI. Успешный init всегда имеет
|
||||
парный uninit, включая ошибку start. Callback и его состояние готовятся до start.
|
||||
Close останавливает/join-ит device worker перед освобождением callback. Отдельный
|
||||
атомарный счётчик кадров контролирует прогресс. Таймер 200 мс проверяет stopped
|
||||
и отсутствие продвижения 1 с; suspend продлевает grace, отдельный xrun не вызывает
|
||||
restart. Backoff: 250/500/1000/2000 мс, сброс после продвижения.
|
||||
|
||||
Miniaudio не добавляет накопитель фиксированного периода: `noFixedSizedCallback`
|
||||
включён, hardware chunks могут иметь любую длину. В PulseAudio физическое начало
|
||||
блока определяется по timing snapshot с AUTO_TIMING_UPDATE/INTERPOLATE_TIMING.
|
||||
Capture: monotonic now минус latency read index, без повторного вычитания длины
|
||||
блока. Playback: now плюс latency write index. Учитываются задержка converter и
|
||||
остающийся input cache. Один backend dispatch имеет один snapshot, позиции
|
||||
последующих client callbacks продвигаются по числу кадров. В отсутствие snapshot
|
||||
используется оценка по callback; источник времени и physical route логируются.
|
||||
PA hole отдельно помечен как отсутствующий capture, а не как реальная тишина.
|
||||
|
||||
`VoiceAudioIo` создаётся только для звонка и рации. Capture и RX обрабатывают
|
||||
независимые потоки. Hardware capture только копирует PCM в фиксированную MPSC-очередь;
|
||||
RX worker выполняет decode/SoundTouch/тоны и поддерживает до 40 мс готового голоса.
|
||||
В сервисном слое рации вычисление Silero держит только TX mutex: общий mutex
|
||||
отпущен, поэтому VAD не блокирует RX; lifetime и порядок PCM/PTT сохранены.
|
||||
Hardware output смешивает готовый PCM. Opus, AEC, VAD, AGC и растущие буферы записи
|
||||
не исполняются в аппаратных callbacks. Обработка самого backend, PCM/resampler
|
||||
узлов miniaudio и атомарные уведомления остаются в callback; строгая hard realtime
|
||||
гарантия не заявляется.
|
||||
|
||||
Capture worker — единственный владелец AEC и порядка PCM/PTT/mute. Кадр AEC —
|
||||
960 отсчётов на канал; 1/2 канала микрофона и 2 независимых канала референса.
|
||||
Противофазное стерео не усредняется. История рендера содержит до 1,92 с PCM и
|
||||
физические timestamps. Reference выбирается по времени микрофона; небольшое
|
||||
различие часов компенсируется интерполяцией только референса. Реальная тишина
|
||||
сохраняется как валидный PCM; при отсутствующем reference AEC пропускает микрофон
|
||||
и не адаптируется. Потеря reference, смена маршрута/поколения и скачок часов
|
||||
сбрасывают фильтр. Возраст capture-очереди ограничен 100 мс от enqueue, отдельно
|
||||
от собственной задержки backend. Потери и очередь видны в сводках категории `aec`.
|
||||
|
||||
Mute применяется после AEC/AGC: фильтр продолжает получать настоящий микрофон,
|
||||
а Opus получает нули только в отмеченной части кадра. Desktop запускает core
|
||||
через `call_audio_start_prepared`: повторного core AEC нет. Raw API с core AEC
|
||||
сохранён для Android/headless; смешивать политики в активном звонке нельзя.
|
||||
|
||||
У рации один автомат передачи: manual PTT или VAD. VAD читает непрерывный очищенный
|
||||
сигнал до AGC, включая время ручного PTT. Передача использует отдельный накопитель
|
||||
Opus-кадра; границы PTT внутри AEC-кадра сохраняются. При закрытии серии хвост
|
||||
дополняется нулями только перед Opus и помещается в очередь перед FIN. Короткий
|
||||
capture tail при stop проходит без AEC, а не дополняет непрерывный фильтр. Разрыв
|
||||
захвата завершает старый сетевой хвост и сбрасывает VAD, исключая склейку через
|
||||
потерянные отсчёты. Capture restart завершает принятую серию; новое прослушивание
|
||||
того же канала отменяет старые BEGIN/media. FIN проверяет токен своего capture/burst
|
||||
и поколение экземпляра ядра, поэтому не закрывает более новую серию и не обращается
|
||||
к освобождённому ядру даже при повторном использовании его адреса.
|
||||
|
||||
TX ограничен 8 media-задачами, возраст — 200 мс. У рации дополнительно ограничено
|
||||
16 control-задач; FIN выделяется заранее вместе с BEGIN. Если control-очередь
|
||||
занята, новый BEGIN откладывается до освобождения; это логируется. Очередь PCM
|
||||
резервирует 8 слотов для управляющих сообщений; producer не меняет consumer и
|
||||
публикует слот только после копирования. Capture callback никогда не ждёт слот.
|
||||
Отказ постановки GUI-команды завершает capture; таймер запускает recovery.
|
||||
|
||||
`AudioRecorder` получает raw microphone без AEC. Его worker обрабатывает AGC,
|
||||
накапливает запись и публикует атомарный уровень/длительность. Переполнение или
|
||||
ошибка обработки отклоняют запись с логом. Meter-only использует тот же capture,
|
||||
но не сохраняет PCM и не создаёт голосовое сообщение. Stop: close device, drain
|
||||
и join worker, затем передача владельца записи задаче attachment. Настройки
|
||||
компрессора в ходе записи применяются к следующей.
|
||||
|
||||
PCM воспроизводится через независимые handles. Сообщение, видео и тест динамика
|
||||
не останавливают чужой PCM; sample rate принадлежит источнику. Seek пересоздаёт
|
||||
узел и очищает его read/resampler cache, сохраняя handle и владельца PCM. Cursor
|
||||
вычисляется по реально обработанным графом кадрам, а не по prefetched datasource.
|
||||
Pause отсоединяет узел и ждёт начатое чтение графа; resume подключает тот же узел,
|
||||
сохраняя его PCM и cache. Соседние проигрыватели продолжают работать.
|
||||
У асинхронного открытия видео/голоса проверяется generation.
|
||||
|
||||
Потоковый AGC использует фиксированные кольца истории и PCM, нулевой lookahead
|
||||
и непосредственный process_frame без растущего output. У записи lookahead явно
|
||||
100 мс; растёт только сохраняемый результат. Push/flush/configure возвращают
|
||||
ошибки; flush проверяется перед кодированием вложения.
|
||||
|
||||
Порядок остановки голосового направления: close capture → join capture worker →
|
||||
detach output → join RX worker → release core. При failed context синхронный сигнал
|
||||
сначала закрывает всех клиентов, затем освобождает context. Mixer, PCM handles,
|
||||
call ID и группа переживают recovery. Stop выключает recovery; таймер завершения
|
||||
звонка проверяет generation. MainWindow останавливает клиентов до SoundManager
|
||||
и сетевого ядра. Сетевые операции и регистрация audio ops выполняются в uasync.
|
||||
|
||||
PulseAudio control-waits ограничены 1 с на операцию; при timeout operation
|
||||
отменяется до возврата, исключая callback с просроченным stack pointer. Partial
|
||||
init освобождает контекст/mainloop/события. Failed stream/context содержит PA
|
||||
errno в логе. Zero writable/zero converter progress возвращают управление.
|
||||
Последовательность нескольких операций init/uninit всё ещё может задержать GUI
|
||||
дольше секунды; отдельный device owner-thread не добавлен.
|
||||
|
||||
## Проверки реализации
|
||||
|
||||
`vibechat` и аудиотесты собираются через CMake. Набор проверки:
|
||||
|
||||
```sh
|
||||
ctest --test-dir tools/chatgui/build --output-on-failure \
|
||||
-R 'test_(linux_audio|audio_timing|audio_diagnostics|audio_recovery|audio_event_queue|voice_audio_io|voice_tx|call_jitter|radio_audio|radio_jitter|voice_file)$'
|
||||
make -C tests test_audio_compressor test_speex_aec
|
||||
./tests/test_audio_compressor
|
||||
./tests/test_speex_aec
|
||||
```
|
||||
|
||||
Добавлены отдельные проверки MPSC (80000 сообщений, reserve/control, слот ещё не
|
||||
опубликован), точных PCM/PTT/mute границ, короткого хвоста, независимого RX при
|
||||
задержке capture worker, противофазного stereo AEC, старого физического timestamp
|
||||
при свежем enqueue, совпадения реальных Opus-пакетов с правильно дополненным
|
||||
хвостом, rapid PTT, capture restart, отмены старого прослушивания и bounded control.
|
||||
Проверяются manual/VAD handover
|
||||
без второй серии и продвижение настоящего сервисного RX при заблокированном VAD.
|
||||
Timing test проверяет read-index timestamp, несколько converter callbacks одного
|
||||
блока и метку hole. Recorder тестирует raw PCM и отсутствие вложения в meter-only.
|
||||
|
||||
Speex проверяет подавление эха и сохранение double-talk также для двух микрофонов
|
||||
с двумя независимыми каналами reference. В детерминированном синтетическом тесте
|
||||
остаточная энергия эха — около 0,009 исходной; корреляция near-end — около 0,95.
|
||||
Эти величины относятся только к тесту, а не к реальным акустическим условиям.
|
||||
|
||||
В `/tmp/utun-audio-asan` проверяются пять тестов нового тракта с ASAN/UBSAN и
|
||||
LeakSanitizer; LeakSanitizer запускается вне песочницы. MPSC отдельно проверяется
|
||||
ThreadSanitizer. Изолированный интеграционный запуск:
|
||||
|
||||
```sh
|
||||
dbus-run-session -- python3 tools/chatgui/tests/run_pulse_recovery.py
|
||||
```
|
||||
|
||||
Итоговый прогон 2026-10-02: сборка `vibechat` успешна, CTest — 11/11,
|
||||
ASAN/UBSAN/LeakSanitizer — 5/5, компрессор — 479/479, Speex — PASS,
|
||||
MPSC под ThreadSanitizer — PASS. Timing-стенд прошёл ещё 100 последовательных
|
||||
повторов. Пауза PCM проверяется также серией pause/resume при работающем соседнем
|
||||
проигрывателе; она ждёт чтение узла, а не только меняет его флаг.
|
||||
|
||||
Тест создаёт собственные PipeWire/PulseAudio/WirePlumber в `/tmp`, отключает
|
||||
аппаратные monitors и использует два виртуальных sink. Проверяет объединение
|
||||
voice с общим выходом после `move-sink-input`, сохранение desired выхода звонка
|
||||
при recovery общего выхода (включая server stream-restore), затем восстановление capture/PCM
|
||||
после остановки и запуска своего PulseAudio. Звонок и рация сохраняют идентичность;
|
||||
recorder и медиа продолжают работу. Системный аудиосервер не затрагивается.
|
||||
|
||||
Исторические проверки исходного recovery: полный `check.sh` — 119 passed,
|
||||
0 failed, 1 skipped; proxy/burst/load интеграции прошли. По указанию пользователя
|
||||
async-тесты исключены из дальнейших запусков. APK ранее собран offline; установка
|
||||
на телефон не выполнялась.
|
||||
|
||||
Физический USB/Bluetooth hotplug, сон ОС, внешние звуки других приложений и качество
|
||||
AEC в длительном реальном звонке этими проверками не подтверждены. Reference
|
||||
охватывает звук этого приложения на том же выходе, согласно выбранному контракту.
|
||||
|
||||
## Период Linux/PulseAudio и сравнительный замер
|
||||
|
||||
`AudioDevice::requestLowLatency()` задаёт запрос 10 мс для capture звонка/рации.
|
||||
Он применяется при каждом открытии, включая recovery и fallback.
|
||||
Остальные backends сохраняют прежние параметры.
|
||||
Общий и отдельный voice output сохраняют прежний период: запрос 10 мс
|
||||
на проверенной системе не снизил измеренную задержку воспроизведения.
|
||||
Кадры AEC/кодека остаются 20 мс; `noFixedSizedCallback` позволяет обслуживать
|
||||
фактические порции backend без дополнительного накопления в miniaudio.
|
||||
|
||||
Это запрос задержки, а не гарантия размера callback. PulseAudio согласует
|
||||
`tlength`, `fragsize`, `minreq`, `maxlength`, `prebuf`; PipeWire также ограничивает
|
||||
quantum и минимальные порции. `AudioDiag` при init выводит внутренний PCM-формат
|
||||
и согласованные параметры в миллисекундах (`-1` — значение по умолчанию).
|
||||
Пересчёт использует внутренние rate/channels/format, а не формат приложения.
|
||||
|
||||
Секундные clock-отчёты содержат `latency_us`/`backend_us` и `block_us` в виде
|
||||
`min/avg/max`. В задержку включены только измеренные timing snapshots;
|
||||
`estimated_blocks`/`measured` позволяют отличить их отсутствие от нулевой задержки.
|
||||
Для времени исполнения callback включать `call=trace`, для состояния AEC —
|
||||
`aec=debug`. Логи агрегируются вне аппаратного callback.
|
||||
|
||||
Ручной режим существующего стенда использует настоящие устройства и голосовые
|
||||
workers, не сохраняет микрофонный PCM и не отправляет его по сети. Выход звонка
|
||||
без удалённого собеседника обслуживается тишиной; акустическое подавление эха
|
||||
этот режим не измеряет. Аргументы: capture frames, playback frames, секунды,
|
||||
необязательный режим `call` (по умолчанию), `radio` или `output`, затем путь лога.
|
||||
Ноль frames оставляет стандартный период backend; override применяется только
|
||||
в тестовом executable, до настоящего `ma_device_init`.
|
||||
|
||||
```sh
|
||||
cmake --build tools/chatgui/build --target test_audio_recovery -j4
|
||||
tools/chatgui/build/test_audio_recovery --latency 960 0 60 > /tmp/audio-baseline.log 2>&1
|
||||
tools/chatgui/build/test_audio_recovery --latency 480 0 60 > /tmp/audio-capture10.log 2>&1
|
||||
tools/chatgui/build/test_audio_recovery --latency 960 480 60 > /tmp/audio-playback10.log 2>&1
|
||||
tools/chatgui/build/test_audio_recovery --latency 480 480 60 > /tmp/audio-both10.log 2>&1
|
||||
```
|
||||
|
||||
Запуски последовательные: несколько потоков могут менять общий quantum сервера.
|
||||
Сравнивать одинаковые маршруты и нагрузку, отдельно от старта/остановки и после
|
||||
стабилизации адаптивного PCM-резерва. Проверять backend latency, возраст первого
|
||||
сэмпла перед готовым TX-кадром, очередь после mix, starvation, holes, потери PCM,
|
||||
AEC reference и событий диагностики. Затем повторить baseline и проверить
|
||||
выбранный вариант под нагрузкой. Меньший возраст PCM в capture callback сам по
|
||||
себе не доказывает такой же выигрыш до кодирования: worker собирает кадры 20 мс.
|
||||
|
||||
`test_voice_audio_io` дополнительно проверяет порции по 256 samples: порядок PCM,
|
||||
временные метки, границы mute/PTT и позицию частичного playback. AEC проверяется
|
||||
при разных порциях capture/reference (256/480 и 480/256) с противофазным stereo
|
||||
reference. Очереди по-прежнему ограничены числом событий; уменьшение периода
|
||||
не увеличивает их ёмкость в миллисекундах.
|
||||
|
||||
Сравнение 2026-10-15 на встроенном HDA, PipeWire 1.6.2, 48 кГц. Каждый вариант
|
||||
работал 60 секунд; таблица содержит средние по callback/кадрам после 45 секунд
|
||||
прогрева (PCM-резерв к этому моменту уменьшился до одного блока).
|
||||
|
||||
| Запрос capture / playback | Capture backend, мс | Первый сэмпл готового TX-кадра, мс | Playback backend, мс | PCM после mix, мс |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| 20 мс / default 25 мс | 30,69 | 34,53 | 16,32 | 18,67 |
|
||||
| 10 мс / default 25 мс | 16,02 | 29,79 | 16,15 | 18,67 |
|
||||
| 20 мс / 10 мс | 31,51 | 34,97 | 18,09 | 24,00 |
|
||||
| 10 мс / 10 мс | 16,13 | 29,66 | 18,46 | 24,00 |
|
||||
|
||||
Изменение capture даёт около 14,7 мс на границе backend и около 4,7 мс перед
|
||||
передачей полного кадра в голосовой стек. Для playback сервер согласовал
|
||||
`tlength=16ms`, `minreq=5.333ms` при запросе 10 мс; callback уменьшился с 512
|
||||
до 256 samples, но выигрыша по задержке не получилось. Поэтому в рабочем коде
|
||||
изменён только capture. Значения в столбцах не следует складывать как независимые
|
||||
участки общей задержки: capture backend входит в возраст TX-кадра, а очередь после
|
||||
mix снята уже после извлечения текущего выходного блока из очереди.
|
||||
|
||||
В этих четырёх прогретых окнах не было потерь capture/reference PCM, выходного
|
||||
starvation, backend underrun/overflow или потери событий диагностики.
|
||||
Отдельные отсутствующие AEC references были и в baseline (4 кадра за окно),
|
||||
и в остальных вариантах (4–5). Это существующее ограничение привязки reference,
|
||||
а не проверка акустического качества. Первоначальный output starvation и
|
||||
временное увеличение PCM-резерва учитываются отдельно от стабильного окна.
|
||||
|
||||
Повтор baseline дал 30,76 мс capture backend и 34,99 мс возраста TX-кадра.
|
||||
Двухминутный прогон выбранного capture 10 мс под нагрузкой четырёх CPU-процессов
|
||||
дал после прогрева 15,81 и 28,55 мс соответственно; PCM после mix — 18,67 мс.
|
||||
Потерь PCM, backend underrun/overflow и потери событий диагностики в этом окне
|
||||
не было. За 75 секунд окна зарегистрировано 25 отсутствующих AEC references.
|
||||
Отдельный минутный прогон рации: capture backend в среднем 15,48 мс, без потерь PCM.
|
||||
|
||||
Сборка `vibechat` и 6 аудиотестов прошли. Изолированный интеграционный тест
|
||||
подтвердил объединение выходных маршрутов и восстановление capture/playback после
|
||||
перезапуска тестового PulseAudio. Запускать весь стенд через `dbus-run-session`,
|
||||
чтобы имя его PulseAudio не конфликтовало с сервером пользовательской сессии.
|
||||
В fixture перед `radio_audio_init` создаётся `radio_instance`, освобождаемый после
|
||||
`radio_audio_destroy`; это исключает ложную ошибку регистрации mute callback.
|
||||
Полный `check.sh` вне sandbox: 156/156, 9/9 proxy-интеграций, burst и load — PASS.
|
||||
В sandbox сетевые тесты не проходят из-за запрета сокетов; этот запуск не является
|
||||
проверкой сетевых регрессий.
|
||||
|
||||
## Референсы
|
||||
|
||||
Первичные исходники и документация просмотрены 2026-10-01; ссылки на master могут
|
||||
изменяться. Используем принципы, а не копируем целиком архитектуру библиотеки.
|
||||
|
||||
- [PipeWire audio-src-ring.c](https://docs.pipewire.org/audio-src-ring_8c-example.html):
|
||||
callback читает доступные кадры, заполняет остаток тишиной и возвращает полный
|
||||
буфер; пополнение инициирует через событие управляющего потока. Хороший минимальный
|
||||
пример поведения при пустой PCM-очереди.
|
||||
- [PulseAudio stream API](https://github.com/pulseaudio/pulseaudio/blob/master/src/pulse/stream.h):
|
||||
отдельные underflow/overflow/started/suspended/moved callbacks; peek различает
|
||||
пустую очередь и hole с ненулевой длиной. Нужно разделять эти состояния в логах.
|
||||
- [PulseAudio pacat.c](https://github.com/pulseaudio/pulseaudio/blob/master/src/utils/pacat.c):
|
||||
underflow callback сообщает событие без пересоздания stream. При failed context
|
||||
утилита завершает работу — это пример диагностики, не готовая self-healing система.
|
||||
- [Mozilla cubeb PulseAudio backend](https://github.com/mozilla/cubeb/blob/master/src/cubeb_pulse.c):
|
||||
явные context/stream states, пересоздание ошибочного context при инициализации,
|
||||
согласованное отключение callbacks и доступ под mainloop lock; minimum latency
|
||||
25 мс. Это полезный референс владения и границ управления, а не основание
|
||||
автоматически заимствовать всю библиотеку.
|
||||
- [miniaudio](https://github.com/mackron/miniaudio/blob/master/miniaudio.h):
|
||||
init/start/stop/uninit запрещены внутри data callback из-за deadlock. Просмотренный
|
||||
upstream тоже содержит break при пустом duplex-ring: обновление библиотеки само
|
||||
по себе не решает этот найденный дефект.
|
||||
@@ -0,0 +1,380 @@
|
||||
Файлы участников CHAT-группы
|
||||
===========================
|
||||
|
||||
Модуль: src/member_files/. Desktop-браузер: tools/chatgui/src/member_files/.
|
||||
Отдельный сервис router ETCP_RT_ID_MEMBER_FILES (0x39), отдельно от вложений
|
||||
сообщений media_delivery и E2E-файлов DM. Серверы выбираются явно; между
|
||||
серверными деревьями автоматической репликации нет.
|
||||
|
||||
Хранение и конфигурация
|
||||
----------------------
|
||||
|
||||
Корень по умолчанию: <db_path>/member_files/ (без db_path — ./member_files/).
|
||||
Публичное дерево: <корень>/<group_id>/<node_id>/<относительный путь>.
|
||||
Временные файлы: <корень>/.uploads/<upload UUID в hex>.
|
||||
При старте сервис создаёт/открывает корень в дисковом worker, удаляет старые
|
||||
незавершённые temp и сканирует опубликованные файлы. До готовности отвечает BUSY.
|
||||
Ошибка сканирования оставляет локальное хранилище недоступным до restart;
|
||||
частичные счётчики клиенту не выдаются. Клиентские передачи к другим серверам продолжают работать.
|
||||
Очистка кэша вложений media/ это дерево не обходит.
|
||||
|
||||
Настройки [chatserver] / [chat]:
|
||||
member_files_enabled=1
|
||||
member_files_user_quota_mb=1024
|
||||
member_files_upload_ttl_sec=3600
|
||||
member_files_path=/путь/к/хранилищу (опционально, до 255 байт UTF-8)
|
||||
|
||||
Путь, квота и TTL читаются при старте чат-сервиса; изменение требует его restart.
|
||||
enabled проверяется на каждом запросе. Лимит одинаков для каждого UID отдельно,
|
||||
размер UID суммируется по всем группам на этом сервере. Значение в МиБ (1024*1024 байт).
|
||||
Счётчики used/reserved/files/directories хранятся в памяти дискового worker и восстанавливаются с диска только при старте.
|
||||
Каталоги не занимают байтовую квоту. files/directories считают опубликованные файлы и каталоги внутри деревьев UID
|
||||
по всем группам; корни групп, корни UID и .uploads исключены. Пустой опубликованный файл считается файлом.
|
||||
Сохранённые файлы удалённых участников также занимают квоту.
|
||||
При начале загрузки резервируется размер нового файла или положительный прирост при замене.
|
||||
WRITE не изменяет used; COMMIT обновляет used на разницу размеров и освобождает резерв.
|
||||
ABORT/Cancel/TTL освобождают резерв после удаления temp; REMOVE вычитает размер файла.
|
||||
Переименование внутри UID не меняет размер и числа объектов. Новый COMMIT увеличивает files на 1;
|
||||
замена существующего файла, включая нулевой размер, сохраняет files. MKDIR увеличивает directories на 1,
|
||||
REMOVE уменьшает соответствующий счётчик. Для mkdir учитывается факт создания даже при последующей ошибке sync.
|
||||
BEGIN/WRITE/ABORT/Cancel/TTL не увеличивают files: temp не опубликован.
|
||||
Все изменения корректируют счётчики инкрементально, без обхода каталога пользователя;
|
||||
QUOTA только читает их из памяти. LIST выполняет свой обычный обход для отображения записей, не пересчитывая статистику.
|
||||
Повторы BEGIN/COMMIT/MKDIR/REMOVE не меняют учёт повторно.
|
||||
Уже превышенная квота не удаляет данные: чтение, удаление и уменьшение файла доступны,
|
||||
новый положительный прирост запрещён. Физический temp может занимать больше места,
|
||||
чем резерв прироста; нехватка места файловой системы остаётся отдельной I/O-ошибкой.
|
||||
Нехватка квоты/места возвращает NO_SPACE; опубликованные файлы не вытесняются.
|
||||
Общая member_files_quota_mb удалена. Корень обслуживается только файловым сервисом;
|
||||
ручные изменения требуют повторного сканирования через restart.
|
||||
|
||||
Сервер — действующий полный мембер группы с подписанным adm_tags.storage="yes".
|
||||
Колонка peers.storage — производный сетевой приоритет, для полномочий не используется.
|
||||
Каждый запрос и ответ подписаны Ed25519 и зашифрованы end-to-end средствами
|
||||
route_crypto. group/src берутся только из проверенного заголовка router;
|
||||
ETCP peer промежуточного хопа не считается автором запроса.
|
||||
Читать могут действующие участники той же группы. Изменять дерево может только
|
||||
src == owner. Удалённые записи и source=1 (плейсхолдеры) доступа не дают.
|
||||
Открытый fd/UUID загрузки не отменяют повторную проверку членства.
|
||||
Файлы удалённого участника сохраняются на диске, но через сервис не доступны.
|
||||
|
||||
Wire, версия 4
|
||||
--------------
|
||||
|
||||
Все числа LE, без сырых структур и padding. Фиксированный заголовок 125 байт:
|
||||
0 protocol_version:u8 = 4
|
||||
1 kind:u8 (1=request, 2=response)
|
||||
2 command:u8
|
||||
3 status:u16 (request=0)
|
||||
5 request_id:u64, ненулевой случайный ID
|
||||
13 owner:u64 (UID каталога; 0 только для LIST виртуального корня группы)
|
||||
21 offset:u64
|
||||
29 size:u64
|
||||
37 cursor:u64
|
||||
45 version:16
|
||||
61 upload:16
|
||||
77 hash:32
|
||||
109 mtime:u64, Unix seconds
|
||||
117 type:u8 (1=file, 2=directory)
|
||||
118 more:u8 (0/1)
|
||||
119 path_len:u16
|
||||
121 target_len:u16
|
||||
123 data_len:u16
|
||||
После заголовка: path[path_len], target[target_len], data[data_len], без NUL.
|
||||
Длина пакета обязана точно совпадать с указанными размерами.
|
||||
Путь максимум 1024 байта, компонент максимум 255 байт, максимум 60 компонентов.
|
||||
Данные максимум 8192 байт; вместе с полным router-путём и crypto укладываются
|
||||
в лимит normalizer 16384 байта. Размер файла/смещение ограничены INT64_MAX.
|
||||
Пути — строгий UTF-8, относительные, разделитель '/'. Запрещены пустые компоненты,
|
||||
компоненты с ведущей '.', завершающей точкой/пробелом, управляющие символы,
|
||||
Windows device names и символы \ : * ? " < > |. Корень внутри UID — пустой путь.
|
||||
POSIX разрешает путь через *at/O_NOFOLLOW; regular-файлы с hardlink отвергаются.
|
||||
Windows удерживает родительские каталоги без SHARE_DELETE и не принимает reparse points.
|
||||
|
||||
Команды
|
||||
---------------
|
||||
|
||||
1 LIST: owner/path/cursor/version. Первая страница cursor=0, version=0.
|
||||
Ответ data — записи: name_len:u16,type:u8,size:u64,mtime:u64,version:16,name.
|
||||
cursor — сколько записей уже выдано. more=1 требует следующую страницу с
|
||||
возвращёнными cursor/version. При изменении снимка — STALE, начать сначала.
|
||||
Размер каталога не ограничивает память страницы. Виртуальный корень группы
|
||||
выдаёт UID действующих полных участников, даже если каталог ещё не создан.
|
||||
Изменение namespace сервера консервативно инвалидирует дисковые LIST-курсоры.
|
||||
|
||||
2 STAT: owner/path. Ответ type/size/mtime/version. Не хеширует весь файл.
|
||||
version — непрозрачный снимок identity/size/mtime/ctime и эпохи сервера.
|
||||
Закрытие fd по таймауту токен не меняет; restart меняет дисковые токены.
|
||||
|
||||
3 READ: owner/path/version/offset/size (1..8192).
|
||||
Ответ size — полный размер файла, data_len — фактически прочитано,
|
||||
more — есть ли байты после ответа. offset==EOF возвращает 0 байт/more=0;
|
||||
offset>EOF — INVALID_RANGE. Изменение файла между блоками — STALE.
|
||||
Клиент не смешивает версии и публикует локальный файл только по завершении.
|
||||
|
||||
4 BEGIN_WRITE: owner/path/upload/size/hash/version.
|
||||
upload — 16 случайных байт, size — конечный размер, hash — SHA256 содержимого.
|
||||
version=0 означает создать новый файл, существующий возвращает EXISTS.
|
||||
Для замены требуется версия из STAT/LIST. В один путь одна активная загрузка.
|
||||
UUID привязан к group/src/owner/path; повтор BEGIN с теми же параметрами безопасен.
|
||||
|
||||
5 WRITE: owner/path/upload/offset/size/data. size==data_len, диапазон внутри
|
||||
конечного размера. Можно писать в произвольном порядке и повторять блок.
|
||||
ACK подтверждает запись в temp, а не durable публикацию всего файла.
|
||||
|
||||
6 COMMIT: owner/path/upload. Проверяет SHA256, fsync/_commit и ожидаемую
|
||||
версию назначения, затем атомарно заменяет файл и синхронизирует namespace.
|
||||
Прежний файл виден до публикации. Ответ содержит размер/версию/SHA256.
|
||||
Незаписанные sparse-области читаются как нули; неправильное содержимое не
|
||||
проходит SHA256. Пустые файлы поддерживаются. При I/O-ошибке во время
|
||||
публикации результат может быть неопределённым; нужно повторно проверить STAT.
|
||||
|
||||
7 ABORT: owner/path/upload. Удаляет temp, освобождает резервацию.
|
||||
Неизвестный UUID — OK; опубликованные файлы никогда не удаляет.
|
||||
|
||||
8 MKDIR: owner/path. Создаёт один каталог; родители должны существовать.
|
||||
9 RENAME: owner/path/target/version. Только внутри UID/группы, без замены
|
||||
существующего назначения. Активная загрузка в затрагиваемом пути — BUSY.
|
||||
10 REMOVE: owner/path/version. Файл или пустой каталог, без рекурсивного удаления.
|
||||
Непустой каталог возвращает DIRECTORY_NOT_EMPTY.
|
||||
|
||||
11 STREAM_WRITE: открытие потоковой загрузки; поля как BEGIN_WRITE,
|
||||
cursor — ненулевой ID потока. Ответ подтверждает готовность temp и конечный размер.
|
||||
12 STREAM_READ: открытие потокового скачивания; owner/path/version/size/cursor,
|
||||
upload — случайный токен клиента. Сервер сверяет снимок и размер перед началом.
|
||||
13 STREAM_DATA: request_id — ID потока, owner/path/upload, offset/size/data.
|
||||
kind=request для upload, response для download. Ответа на блок нет.
|
||||
offset обязан равняться следующему принятому смещению; size==data_len, не более 8192.
|
||||
14 STREAM_END: ID потока, owner/path/upload, offset==size==размер файла, SHA256.
|
||||
Upload: клиент отправляет END после всех DATA, сервер отвечает END после COMMIT.
|
||||
Download: сервер отправляет END после DATA и повторной проверки версии; клиент
|
||||
проверяет SHA256 записанных данных, синхронизирует и публикует локальный файл.
|
||||
Ошибка сервера приходит END.status!=OK; ошибка открытия — ответом на STREAM_READ/WRITE.
|
||||
15 STREAM_CANCEL: клиент отменяет конкретный поток. Поздние DATA не публикуются.
|
||||
|
||||
16 QUOTA: owner=UID, path/target пустые, cursor=0, data_len=0.
|
||||
Права чтения те же, что у STAT/LIST выбранного участника. Ответ data_len=40:
|
||||
used:u64, limit:u64, reserved:u64, files:u64, directories:u64, LE.
|
||||
Первые три значения в байтах, последние два — числа объектов. Снимок общий для UID во всех группах.
|
||||
Не резервирует место и не занимает квитанцию мутации. used может превышать limit
|
||||
после уменьшения лимита; сервер не выдаёт фиктивный нулевой размер при ошибке учёта.
|
||||
|
||||
Открытие потока — обычный повторяемый запрос с отдельным request_id. cursor связывает
|
||||
его с DATA/END/CANCEL. Поток определяется group/peer/kind/ID; один ID не может
|
||||
одновременно обозначать разные открытия. Каждый пакет по-прежнему подписан и зашифрован.
|
||||
При закрытии/перезапуске router-канала его handles завершаются с IO_ERROR;
|
||||
байты разных эпох не смешиваются. Возобновление через новую эпоху пока не реализовано.
|
||||
|
||||
Статусы (0..14): OK, ACCESS_DENIED, NOT_FOUND, INVALID_PATH, INVALID_RANGE,
|
||||
EXISTS, BUSY, STALE, NO_SPACE, IO_ERROR, BAD_PACKET, HASH_MISMATCH, CANCELLED, TIMEOUT,
|
||||
DIRECTORY_NOT_EMPTY.
|
||||
Мутации COMMIT/MKDIR/RENAME/REMOVE сохраняют успешный ответ на 120 секунд
|
||||
(64 квитанции); тот же request_id с другим содержимым — BAD_PACKET.
|
||||
COMMIT дополнительно узнаётся по upload UUID. При заполнении квитанций новая
|
||||
мутация возвращает BUSY до истечения старых. Клиент повторяет запрос с прежним
|
||||
request_id раз в 5 секунд, общий срок 120 секунд. Повторы после этого срока
|
||||
не дают гарантии того же результата; перед новой мутацией требуется STAT.
|
||||
|
||||
Потоки, таймауты и остановка
|
||||
---------------------------
|
||||
|
||||
На экземпляр один дисковый pthread с очередью до 64 обычных задач. Сеть,
|
||||
ACL, таймеры и state machines выполняются в uasync; open/stat/readdir/read/write,
|
||||
resize/hash/sync/rename/remove/close — в дисковом потоке. GUI только передаёт
|
||||
команды через bridge и получает события. Нет вызова дискового I/O из callback
|
||||
таймера. Completion резервируется до постановки задачи; без polling workers.
|
||||
Локальные файлы transfer и серверное дерево используют тот же дисковый поток.
|
||||
Для обязательной очистки резервируется отдельная задача до открытия файла.
|
||||
Обращение к своему серверу доставляется через отдельную ограниченную очередь
|
||||
call_soon в тот же обработчик, без сетевого канала. ACL и дисковый поток общие;
|
||||
сетевой recv не принимает unsigned даже при src == self.
|
||||
|
||||
Сервер держит до 64 открытых файлов. Каждый READ/WRITE/BEGIN продлевает idle.
|
||||
После 10 секунд простоя fd закрывается на очередном дисковом sweep (таймер 1 с);
|
||||
занятый дисковый поток откладывает sweep до освобождения. Закрытие не публикует
|
||||
и не удаляет файл. Temp можно открыть заново до истечения upload TTL.
|
||||
После restart незавершённые temp удаляются; опубликованные файлы остаются.
|
||||
|
||||
Лимиты: 16 клиентских запросов, 32 серверных задания/ответа, 16 временных
|
||||
загрузок, 16 клиентских и 16 серверных потоков на экземпляр. Каждый поток имеет
|
||||
кольцо 32 x 8192 байт (256 КиБ); worker обрабатывает до 16 блоков за задачу.
|
||||
Чтение следующей порции перекрывается с отправкой уже подготовленной.
|
||||
Все серверные и локальные I/O используют один FIFO worker, без блокировки uasync.
|
||||
|
||||
Отправитель добавляет пакеты, пока send_q router находится под нижним порогом
|
||||
(сейчас count<=0), затем ждёт etcp_router_on_send_ready. Файловый слой не устанавливает
|
||||
своё окно inflight и не повторяет DATA; транспортные ACK, retransmit и порядок — router.
|
||||
Callback отправляет не более 8 блоков за запуск, уступая очередь другим потокам.
|
||||
Параллельные файлы одного пира разделяют канал и FIFO/round-robin waiters.
|
||||
|
||||
Готовность приёмника проверяется router до доставки следующего seq. Заполненное
|
||||
кольцо или очередь заданий оставляют пакет в recv_q: rx_seq/ACK не продвигаются.
|
||||
Освобождение места вызывает отложенный resume; pump отменяется при reset/close.
|
||||
ACK RECEIVE_BUSY (flags bit0) раз в 100 мс отличает ожидание диска от потери транспорта.
|
||||
Отправитель приостанавливает новые DATA, а живые busy ACK продлевают таймаут router.
|
||||
Буфер router ограничен его receive window. Локальная доставка использует такую же
|
||||
проверку готовности и ограниченный FIFO, а не сеть или отдельные подтверждения.
|
||||
|
||||
При остановке сначала отменяются producers/requests и дожидаются все stream cleanup
|
||||
continuations; только затем worker освобождает storage. Отмена после уже выполненной
|
||||
публикации не удаляет опубликованный файл.
|
||||
|
||||
Transfer владеет CM handle до окончания/отмены; DOWN/TIMEOUT handle не освобождает.
|
||||
quiesce запрещает новые команды, отменяет таймеры, waiter и запросы; destroy
|
||||
вне callbacks дожидается только своих работ/completions и закрывает дисковый
|
||||
поток до уничтожения CHAT-групп. Контексты не освобождаются под работающим I/O.
|
||||
Отмена не прерывает текущий системный I/O или хеширование, а ждёт его completion.
|
||||
|
||||
Desktop
|
||||
-------
|
||||
|
||||
Контекстное меню канала: Group files. Меню участника: Member files.
|
||||
Окно: выбор сервера/UID, путь, Up/Refresh, таблица каталогов/файлов,
|
||||
Upload/Download/New folder/Rename/Delete, прогресс и Cancel transfer.
|
||||
Чужой UID доступен только для чтения. Автор может быть offline.
|
||||
Open скачивает в выбранный пользователем локальный файл и открывает его
|
||||
приложением ОС после завершения. Смена контекста отбрасывает поздние ответы.
|
||||
Для выбранного UID строка хранилища показывает занято/всего, число файлов/каталогов и резерв текущих загрузок.
|
||||
Точные байты доступны в tooltip. QUOTA запрашивается отдельно от LIST при выборе
|
||||
сервера/UID, обновлении каталога, завершении передачи и каждые две секунды видимого окна.
|
||||
Поздние ответы старого UID/сервера игнорируются. BUSY при стартовом сканировании
|
||||
сохраняет Loading directory и повторяет LIST через секунду; новый переход отменяет повтор.
|
||||
|
||||
API параллельных передач
|
||||
-----------------------
|
||||
|
||||
member_files_transfer_start(request, callback, arg) копирует request и возвращает
|
||||
отдельный mf_transfer* на каждый файл. upload задаёт направление, group/server/owner
|
||||
выбираются для каждой операции. id=0 выбирает случайный ID; id в callback — фактический.
|
||||
NULL означает отказ запуска без callback; причина пишется в лог.
|
||||
max_bytes=0 оставляет прежний лимит; ненулевое значение ограничивает размер файла.
|
||||
Upload проверяет размер источника до хеширования, download — ответ STAT до открытия
|
||||
локального temp. STREAM привязан к проверенным размеру/version; превышение — INVALID_RANGE.
|
||||
|
||||
callback(arg, mf_transfer_progress*) получает id/done/total/status/finished.
|
||||
Прогресс заимствован до возврата callback. done для отправителя означает байты,
|
||||
принятые router в очередь; успешный finished подтверждает завершение операции,
|
||||
для upload — публикацию на сервере, для download — публикацию локального файла.
|
||||
finished приходит ровно один раз, handle к этому моменту уже освобождён;
|
||||
вызывающий обнуляет свой указатель в callback. member_files_transfer_cancel(handle)
|
||||
отменяет только этот файл, ждёт его I/O и ставит CANCEL в очередь с backpressure.
|
||||
Stop сервиса/группы завершает все принадлежащие ему handles.
|
||||
|
||||
Все API/callbacks выполняются в uasync-потоке экземпляра. GUI trampolines используют
|
||||
тот же API и превращают callback в существующие события прогресса; Qt не делает I/O.
|
||||
Один локальный файл назначения не может одновременно принадлежать двум download.
|
||||
|
||||
Аватарки пользователей
|
||||
---------------------
|
||||
|
||||
Относительный путь avatars/<sha256>.<расширение> находится внутри обычного дерева UID:
|
||||
<корень>/<group>/<UID>/avatars/<sha256>.<расширение>. Дополнительного UID в API/пути нет.
|
||||
Аватарка по умолчанию копируется в каждую группу, где выбранный сервер имеет
|
||||
подписанную storage-роль. Ссылка каждого канала публикуется только после COMMIT
|
||||
соответствующей копии; отсутствующий сервер в другом канале не создаёт битую ссылку.
|
||||
Новое членство обнаруживается через снимки участников и повторяет публикацию.
|
||||
Между серверами автоматического копирования нет.
|
||||
|
||||
userinfo содержит строковые avatar_server (16 hex цифр) и avatar_file (64 hex SHA256 + .json).
|
||||
Оба поля отсутствуют при удалённой аватарке. Профиль подписан ключом участника;
|
||||
смена имени/адресов сохраняет остальные плоские JSON-атрибуты. userinfo ограничен
|
||||
1024 байтами UTF-8. Merkle, JOIN_INFO_RESP, CHANNEL_INVITE и JOIN_REQUEST используют
|
||||
длину userinfo:u16 BE вместо прежнего u8. Bootstrap передаёт существующий подписанный
|
||||
профиль инвайтера, не создавая более свежую запись только с именем. Обе стороны
|
||||
нужно обновить: обратной совместимости нет.
|
||||
|
||||
Снимок участника CHAT_MEMBER_DISPLAY_SIZE теперь 158 байт: прежние 79 байт,
|
||||
avatar_server:8LE, avatar_file:70 (с NUL), storage_role:1. storage_role обозначает
|
||||
подписанную роль сервера, независимо от существующей колонки storage — сетевого
|
||||
приоритета, который может быть 0 у сервера за NAT. Потребители используют общий
|
||||
chat_member.h; локальное изменение профиля также вызывает MEMBER_UPDATED.
|
||||
|
||||
В Settings → Profile доступны Upload avatar, выбор storage-сервера, предпросмотр
|
||||
и Remove avatar. Изменения применяются сразу, независимо от Save других настроек.
|
||||
Перед загрузкой desktop/Android показывают редактор: неподвижная квадратная рамка,
|
||||
перемещение фото и масштабирование (мышь/колесо или жесты). Отмена не публикует новую ссылку.
|
||||
Worker принимает JPEG/PNG/WebP до 32 МиБ и 16 мегапикселей. Исходные байты, включая EXIF,
|
||||
сохраняются без изменений. Выбранная область после EXIF-ориентации становится WebP lossy
|
||||
256×256, quality=85, максимум 1 МиБ. Описание JSON (максимум 4096 байт): version=1,
|
||||
original/preview — имена объектов; original_bytes/preview_bytes, width/height — размеры
|
||||
ориентированного оригинала; x/y/w/h — нормализованный квадрат в его координатах.
|
||||
Все объекты лежат в avatars/<SHA256 собственных байтов>.<jpg|png|webp|json>.
|
||||
Порядок COMMIT: оригинал, превью, описание; затем подписанная ссылка на описание.
|
||||
Повтор использует уже сохранённые неизменяемые объекты. Сервер проверяет имя/SHA256,
|
||||
лимит объекта и запрещает замену под прежним именем и rename в/из avatars.
|
||||
Файлы и каталог входят в обычные квоты. Прежние версии сохраняются на сервере;
|
||||
владелец может удалить их через файловый браузер. Старые PNG-ссылки не поддерживаются.
|
||||
|
||||
Клик по аватарке сообщения/участника открывает галерею пользователя: LIST каталога
|
||||
выбирает только описания, проходит все страницы cursor/version, сортирует mtime DESC,
|
||||
при равенстве — по имени. Изначально выбрана текущая подписанная ссылка. Большое фото —
|
||||
полный оригинал, до его загрузки показывается превью. Внизу пять квадратных позиций:
|
||||
две предыдущие, выбранная по центру, две следующие; за границами списка позиции пустые.
|
||||
Desktop: кнопки/клавиши ←/→; Android: кнопки/свайпы при исходном масштабе. Превью кликабельны.
|
||||
Загружаются выбранный оригинал и видимые превью; закрытие отменяет запросы без других владельцев.
|
||||
Для DM источник — общий канал, из которого берётся отображаемая аватарка.
|
||||
|
||||
AvatarManager общий для списка участников и сообщений, максимум две операции
|
||||
одновременно, одинаковые хеши не скачиваются параллельно. Изменение своего профиля
|
||||
имеет приоритет перед фоновыми загрузками. Для DM используются ссылки из общих
|
||||
каналов; при отсутствии доступной ссылки остаются инициалы. Онлайн-статус списка
|
||||
отображается отдельной точкой поверх фотографии. Поздний результат старой ссылки
|
||||
пополняет кеш, но не заменяет картинку новой ссылки.
|
||||
|
||||
Кеш аккаунта: <db_path>/avatar_cache/<local_UID_hex>/<sha256>.<расширение>; без db_path
|
||||
используется Qt AppLocalDataLocation. Проверяются SHA256 объектов, описание и WebP 256×256;
|
||||
превышение/битая картинка не публикуются. Публикация кеша атомарна через QSaveFile,
|
||||
незавершённые download/part очищаются при старте. Готовые файлы не требуют TTL/STAT:
|
||||
смена содержимого меняет имя. Дисковый LRU — 100 МиБ, кеш QImage — 32 МиБ;
|
||||
собственная аватарка и источники незавершённых upload сохраняются при вытеснении.
|
||||
Дисковое I/O и декодирование выполняются вне GUI/uasync. Повторы неудачных загрузок:
|
||||
5/10/20/40/80/160 секунд; новая ссылка сбрасывает задержку. RTT/online не меняют ссылку
|
||||
и не вызывают повторной загрузки. Желаемая собственная ссылка/удаление сохраняются
|
||||
атомарно одной строкой ui_state.avatar_default, незавершённая публикация повторяется
|
||||
после запуска при наличии локального источника.
|
||||
|
||||
test_avatars проверяет нормализацию, хеш/размеры, очистку temp, настоящее storage
|
||||
в двух каналах, публикацию, сохранение при смене имени, DM-кеш, restart без сети,
|
||||
автоматический download после потери кеша и удаление ссылок. Также проверяются
|
||||
max_bytes до открытия local temp и смена аватарки при задержанном download старой
|
||||
версии: поздний результат не заменяет новый рисунок. test_member_adapter проверяет
|
||||
подписанные профили 191/255/256/512/1023/1024 байт вместе с adm_tags до 8192.
|
||||
Категория media=debug показывает discovery, cache hit/eviction, направление,
|
||||
group/owner/server/hash, ошибки проверки и retry; media=info — передачи/публикации.
|
||||
|
||||
Проверки и диагностика
|
||||
---------------------
|
||||
|
||||
tests/test_member_files: wire/UTF-8, произвольные блоки, повторы, EOF, версии,
|
||||
короткая замена, SHA256, пустой файл, независимые квоты UID, общий учёт по группам,
|
||||
резервирование/повторы/TTL, инкрементальные files/directories, замены пустых файлов,
|
||||
создание/удаление/rename каталогов, отсутствие runtime-rescan, стартовый scan и его ошибки, ссылки, expiry/restart, 10-секундный
|
||||
fd idle и принадлежность work/done потокам.
|
||||
tests/test_member_files_network: настоящие A/B/M, подписанная storage-роль,
|
||||
параллельные upload/download одного пира, local loopback, пустые файлы, побайтная
|
||||
проверка, отмена одного handle, restart router и destroy с активным server I/O.
|
||||
Шестисекундная задержка worker проверяет receive backpressure и busy ACK.
|
||||
Также проверяются чужая запись, виртуальный корень и подписанное удаление читателя.
|
||||
QUOTA проверяется на настоящем подписанном/зашифрованном канале; удалённый читатель не получает счётчики.
|
||||
test_services — stop/start общего чат-сервиса.
|
||||
Диагностика файлового сервиса — категория media, маршрутизатора — etcp_route.
|
||||
Настройка для полного трасса этих модулей:
|
||||
[debug]
|
||||
media=trace
|
||||
etcp_route=trace
|
||||
media=debug показывает запрос/ответ, ACL, очереди файлового сервиса, I/O и fd idle.
|
||||
etcp_route=debug добавляет выбор маршрута, состояния канала и ретрансмиты.
|
||||
Уровень trace дополнительно показывает каждый
|
||||
read/write и передачу задач/завершений между дисковым потоком и uasync.
|
||||
stream progress раз в секунду показывает ID/направление, done/total, число блоков
|
||||
в кольце, активность I/O, причину ожидания и скорость B/s. Router логирует переходы
|
||||
receive paused/resumed и peer receive busy без повторного сообщения на каждый блок.
|
||||
quota scan started/ready показывают путь, число пользователей и время обхода;
|
||||
quota updated показывает UID/операцию/used/reserved/files/directories; quota exceeded — used/reserved/growth/limit.
|
||||
По request_id видны GUI post → core request → router seq → server rx → ACL →
|
||||
disk queued/start/done → response tx → GUI result/bridge → directory ready.
|
||||
PID создания worker, PID отправки задачи и startup PID позволяют однозначно
|
||||
обнаружить fork после создания потока; несовпадение PID логируется как ошибка.
|
||||
Ключи шифрования, plaintext файлов и сырые медиа в debug не выводятся.
|
||||
Старые VAD/рация/GUI/вложения сообщения перенесены в vad/radio/chat/media/config;
|
||||
ошибки и пользовательские info остаются в тематических категориях.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Ссылки в сообщениях
|
||||
|
||||
Android и Qt распознают `http://`, `https://` и `utun://` в обычном тексте
|
||||
личных и групповых сообщений. HTTP/HTTPS нечувствительны к регистру схемы.
|
||||
Ссылки выделяются цветом и подчёркиванием; длинные веб-адреса переносятся по ширине.
|
||||
Сокращённая строка структурированного PM-приглашения сохраняет полный `utun://` адрес.
|
||||
|
||||
Пробелы, управляющие символы, кавычки и угловые скобки отделяют ссылку от текста.
|
||||
Конечные `. , ; : ! ?` и лишние закрывающие скобки удаляются из распознанного адреса.
|
||||
Сбалансированные скобки в адресе, параметры, fragment и Unicode сохраняются.
|
||||
Адреса без схемы не распознаются. Некорректные веб-адреса остаются обычным текстом.
|
||||
Исходное сообщение в БД и протоколе не меняется.
|
||||
|
||||
Разборщики возвращают диапазоны UTF-16 и полные адреса:
|
||||
`tools/chatgui/src/message_links.cpp`, Android `data/MessageLinks.kt`.
|
||||
Подсветка и нажатие используют эти диапазоны. Android подставляет emoji placeholders
|
||||
только в обычный текст между ссылками: emoji внутри адреса остаётся частью полного URL.
|
||||
Qt использует один QTextLayout для размеров, отрисовки, выделения и координат нажатия.
|
||||
Перетаскивание выделяет текст без открытия ссылки.
|
||||
|
||||
HTTP/HTTPS открываются из GUI системным обработчиком: Android `ACTION_VIEW`,
|
||||
Qt `QDesktopServices::openUrl`. `utun://` запускает существующий JOIN.
|
||||
Ошибки открытия показываются через перевод интерфейса и логируются.
|
||||
Диагностика открытия содержит схему и host, без полного пути/query.
|
||||
|
||||
Общие случаи: `tools/chatgui/tests/message_links_cases.tsv`.
|
||||
Qt `test_invite_links` проверяет разбор, перенесённые веб-ссылки, смесь схем,
|
||||
нажатие, drag/copy и сокращённые приглашения; `test_emoji_playback` — жесты приглашений.
|
||||
Android `tests/test_message_links.kt` проверяет тот же TSV на классах из APK-сборки:
|
||||
|
||||
```bash
|
||||
cd tools/chatgui-android
|
||||
./build.sh noinstall
|
||||
kotlinc tests/test_message_links.kt -classpath app/build/tmp/kotlin-classes/debug -d /tmp/message-links-tests.jar
|
||||
kotlin -classpath /tmp/message-links-tests.jar:app/build/tmp/kotlin-classes/debug Test_message_linksKt ../chatgui/tests/message_links_cases.tsv
|
||||
```
|
||||
@@ -0,0 +1,109 @@
|
||||
Как работает детектор NAT
|
||||
========================
|
||||
|
||||
Детектор выясняет две вещи: какой внешний IP:порт получает рабочий UDP-сокет
|
||||
при обращении к разным IP и проходят ли к нему пакеты от незнакомого IP.
|
||||
Проверяется именно сокет, через который работает приложение.
|
||||
|
||||
Есть быстрый путь без помощников: если рабочий сокет принял подключение
|
||||
и локальный IP и фактический IP подключившегося узла публичные,
|
||||
ему сразу ставится direct/open (UNCHANGED + OPEN). Это правило действует
|
||||
для UDP и TCP, даже если с IP пира раньше уже был исходящий обмен.
|
||||
Локальные подключения и SOCKS интернет-вход не подтверждают. Для TCP
|
||||
фактический локальный IP должен совпадать с IP интерфейса сокета.
|
||||
|
||||
Нужны три узла: A проверяет себя, B наблюдает его внешний адрес,
|
||||
C помогает проверить входящие пакеты. Команды и результаты передаются
|
||||
по уже работающей сети uTun, в том числе через TCP или промежуточные узлы.
|
||||
Сами тестовые пакеты идут напрямую по UDP.
|
||||
|
||||
Порядок проверки
|
||||
---------------
|
||||
|
||||
1. A отправляет пакет узлу B. Тот сообщает, с какого IP:порта его получил.
|
||||
Ответ идёт по управляющему соединению, поэтому блокировка прямого
|
||||
UDP-ответа не мешает узнать внешний адрес A.
|
||||
|
||||
2. Через B узел A просит C отправить несколько пакетов на этот внешний адрес.
|
||||
До этого A не должен отправлять ничего на IP узла C с проверяемого сокета.
|
||||
Приложение хранит историю таких отправок и учитывает новые во время проверки.
|
||||
На входящие тестовые пакеты A автоматически не отвечает.
|
||||
|
||||
3. Получатель проверяет настоящий адрес отправителя каждого тестового пакета.
|
||||
Локальный или другой непубличный IP не подходит для интернет-проверки.
|
||||
Для проверки незнакомого входа также не подходят IP узла B, собственный
|
||||
внешний IP узла A и IP, на который A уже отправлял пакеты.
|
||||
Если помощник не подходит, A выбирает другого. Разные узлы с одним IP
|
||||
не считаются разными источниками. Проверяются также подлинность пакета
|
||||
и принадлежность текущей попытке, чтобы чужой или старый пакет не влиял на результат.
|
||||
|
||||
4. Затем A сам отправляет пакет C и узнаёт, какой внешний IP:порт видит C.
|
||||
Если входящие до этого не проходили и внешний адрес совпал с увиденным B,
|
||||
C повторяет отправку с того же IP:порта на исходный внешний адрес A.
|
||||
Так проверяется, открыл ли исходящий контакт путь для входящих.
|
||||
Параллельный обмен с B подтверждает, что UDP-связь в целом работает.
|
||||
|
||||
5. A повторяет измерение внешнего адреса через B и C. Результат принимается,
|
||||
только если адрес для каждого направления остаётся стабильным.
|
||||
Сравниваются разные IP назначения; другие порты того же IP не проверяются.
|
||||
|
||||
Что означает результат
|
||||
----------------------
|
||||
|
||||
strict — B и C стабильно видят разные внешние IP:порты узла A.
|
||||
Единого внешнего адреса для подключения с любого IP нет.
|
||||
|
||||
eim open — B и C видят одинаковый внешний IP:порт, и пакет от C пришёл
|
||||
ещё до исходящего контакта A с его IP.
|
||||
|
||||
eim filtered — внешний IP:порт одинаковый, серия пакетов до контакта не прошла,
|
||||
а после исходящего контакта пакеты с того же адреса C начали приходить.
|
||||
Это оценка: потерю UDP-пакетов нельзя абсолютно отличить от блокировки.
|
||||
|
||||
EIM? — два направления видят одинаковый внешний IP:порт, но незнакомый вход
|
||||
проверить не удалось, например все доступные помощники уже были знакомы.
|
||||
Это EIM с неизвестной доступностью входящих, без обещания открытого входа.
|
||||
Такой же предварительный EIM? дают два STUN-сервера, видящие одинаковый внешний
|
||||
IP:порт тестового сокета на том же локальном IP и интерфейсе. Поддержка запроса
|
||||
с незнакомого IP для этого не нужна. Источник оценки — STUN, вход остаётся UNKNOWN.
|
||||
Измерение рабочего сокета имеет приоритет; STUN-оценка действует до пяти минут
|
||||
и обновляется автоматически. Публичный порт для подключения берётся только
|
||||
из наблюдения рабочего сокета: внешний порт STUN в него не подставляется.
|
||||
|
||||
Если внешний IP:порт совпадает с локальным, отдельно отмечается отсутствие
|
||||
трансляции адреса. Доступность входящих всё равно проверяется.
|
||||
|
||||
Если данных недостаточно, соответствующая часть результата остаётся UNKNOWN.
|
||||
Известный тип NAT можно сохранить, даже если входящие проверить не удалось.
|
||||
Таймаут и отсутствие подходящих узлов сами по себе не означают strict.
|
||||
|
||||
Если один пир уже увидел публичный IP:порт, отличный от локального, этот адрес
|
||||
сразу сохраняется как NAT с type=UNKNOWN. Он публикуется отдельно от адреса
|
||||
интерфейса и остаётся кандидатом для пробного подключения: одного наблюдения
|
||||
недостаточно, чтобы обещать тот же адрес для других IP или открытый вход.
|
||||
Уже знакомый помощник пригоден для измерения типа NAT, хотя проверить незнакомый
|
||||
вход через него нельзя. Потеря результата входящих не останавливает сравнение
|
||||
адресов через доступные публичные узлы; ожидание одного этапа ограничено по времени.
|
||||
Измеренный mapping сохраняется сразу, а проверка незнакомого входа продолжается
|
||||
через остальных помощников. Порядок обхода фиксирован на его начало; фактический
|
||||
source всегда проверяется по актуальной истории. При паузе по общему бюджету
|
||||
незавершённая попытка повторяется с новыми идентификаторами, без сброса обхода.
|
||||
Если попытка уже получила полный бюджет, его исчерпание считается таймаутом:
|
||||
обход переходит дальше, а не повторяет одного помощника бесконечно.
|
||||
Старый сохранённый результат входящих не считается успехом новой проверки.
|
||||
|
||||
Соединения по LAN работают независимо и не сбрасывают интернет-профиль.
|
||||
Если доступны только локальные узлы, измерить тип интернет-NAT для UDP нельзя.
|
||||
STUN на другом порту даёт предварительную оценку типа NAT для того же IP/интерфейса.
|
||||
Работа TCP не заменяет проверку UDP.
|
||||
|
||||
Для TCP определяется только наличие NAT. Непубличный локальный IP сразу означает
|
||||
NAT с неизвестным типом. Если локальный IP публичный, достаточно одного публичного
|
||||
TCP-пира: по готовому защищённому соединению он сообщает увиденный IP.
|
||||
IP совпали — трансляция IP не обнаружена; отличаются — NAT есть, тип UNKNOWN.
|
||||
Порты при сравнении не учитываются. Публичный внешний IP сохраняется с портом 0:
|
||||
входящий порт неизвестен, поэтому NCD не использует эту запись для подключения.
|
||||
Активных проб типа NAT и TCP-входа нет; OPEN устанавливается по принятому
|
||||
публичному подключению и сохраняется при последующих совпавших наблюдениях IP.
|
||||
Через SOCKS внешний IP прокси
|
||||
не записывается как собственный адрес.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Подписанная запись узла
|
||||
|
||||
`TOPO_NODE.timestamp` — версия всей подписанной записи, а не отдельного адреса.
|
||||
Узел-владелец назначает Unix microseconds, строго больше предыдущей сохранённой
|
||||
версии. Имя, тип, активность, адреса, метаданные сокетов, REALITY и подпись
|
||||
заменяются вместе. Timestamp входит в подпись. Групповые пути и подсети
|
||||
остаются в `TOPO_GROUP_NODE` и не являются частью общей записи узла.
|
||||
|
||||
Получатель проверяет подпись до изменения реестра или маршрутов. Более свежая
|
||||
запись заменяет старую целиком, включая пустые списки адресов. Старые записи
|
||||
игнорируются; разные подписанные записи с одинаковым timestamp — конфликт.
|
||||
Повторное BGP-объявление может обновить путь, но не содержимое общей записи.
|
||||
8-битный `ver` сохраняется как диагностический счётчик; свежесть определяется
|
||||
только timestamp.
|
||||
|
||||
Живая телефонная занятость передаётся без изменения структуры NODEINFO: в существующем
|
||||
`flags` бит `0x04` означает известное состояние, `0x08` — занятость звонком.
|
||||
Это групповые флаги `TOPO_GROUP_NODE.phone_status`, как флаг рации `0x02`;
|
||||
они не входят в подпись общей записи и используются только для цвета трубки в GUI.
|
||||
Без разрешения Android, при выключенной публикации и после ухода узла статус неизвестен.
|
||||
Решение о начале вызова принимает адресат по локальному состоянию ОС, а не по BGP-подсказке.
|
||||
Изменения публикуются обычной новой версией NODEINFO во всех CHAT-группах; отдельного опроса и таймера нет.
|
||||
|
||||
Профиль endpoint в SOCKMETA содержит независимые `mapping` и `inbound`, источник `source`.
|
||||
ID и время измерения не передаются: последний успешный профиль действует до
|
||||
явного изменения endpoint или подтверждённого нового результата. Повторная
|
||||
проверка с тем же результатом не меняет NODEINFO. Все поля SOCKMETA — отдельные байты;
|
||||
границы списков metadata/адресов/REALITY также входят в каноническую подпись.
|
||||
Алгоритм и условия неопределённости описаны в `doc/nat_detection.txt`.
|
||||
`source=ETCP` обозначает результат детектора рабочего endpoint. Одно UDP-наблюдение может дать
|
||||
адрес `TOPO_ADDR_NAT` при `mapping=UNKNOWN`, `inbound=UNKNOWN`: это NAT-кандидат
|
||||
для пробного подключения, а не адрес интерфейса или подтверждённый открытый вход.
|
||||
Общий внешний адрес подтверждает EIM; strict не публикует универсальный NAT-адрес.
|
||||
Source входит в подпись; SOCKMETA занимает 5 байт. `source=STUN` означает предварительный
|
||||
EIM? для того же локального IP/netif с неизвестным входом; измерение ETCP приоритетно.
|
||||
Владелец снимает STUN-оценку после TTL. Внешний адрес тестового порта не публикуется:
|
||||
NAT-кандидат по-прежнему требует наблюдения рабочего сокета.
|
||||
Для TCP достаточно одного публичного пира: внешний IP с неизвестным входящим
|
||||
портом публикуется как `TOPO_ADDR_NAT` с `port=0`, `mapping=UNKNOWN`, `inbound=UNKNOWN`.
|
||||
NCD пропускает нулевые порты. Непубличный локальный TCP IP сразу означает NAT;
|
||||
совпавший с наблюдением публичный IP даёт UNCHANGED без подтверждения открытого входа.
|
||||
|
||||
Таблица `node_snapshots` хранит полную сериализованную запись без группового
|
||||
окружения. `node_addresses` остаётся проекцией для поиска и отображения, а не
|
||||
источником для восстановления подписанной записи. При наличии сохранённого
|
||||
snapshot загрузка использует его. Локальная публикация сохраняется до рассылки;
|
||||
ошибка сохранения не разрешает публикацию новой версии.
|
||||
|
||||
В проекции адресов сохраняются `nat_mapping`, `nat_inbound`, `nat_source`, `address_kind`.
|
||||
Уникальность определяется endpoint и видом адреса,
|
||||
а не отображаемым классом NAT. Старый производный формат пересоздаётся;
|
||||
bootstrap-загрузка без подписанного snapshot не подтверждает NAT-профиль.
|
||||
При удалении старых `nat_evidence`/`nat_checked` адреса и REALITY сохраняются для bootstrap,
|
||||
старые профили сбрасываются, сериализованные snapshots инвалидируются.
|
||||
Максимальные timestamp snapshots сохраняются, чтобы новая локальная публикация
|
||||
не откатывала версию даже при коррекции часов. Чат и ключи миграция не изменяет.
|
||||
|
||||
Timestamp 0 обозначает bootstrap-сведения без подписи (конфиг, приглашение,
|
||||
начальные адреса). Они могут помочь установить первое соединение, но не
|
||||
заменяют принятую подписанную запись. По BGP принимаются подписанные записи
|
||||
с ненулевым timestamp. Формат NODEINFO изменён без обратной совместимости.
|
||||
|
||||
После принятия BGP-записи `node_conn_direct_update_node` применяет адреса к
|
||||
существующему NCD-соединению. Отсутствующее соединение этот вызов не создаёт.
|
||||
Линки дополняются как при UP, так и до установления соединения; повторная
|
||||
доставка не создаёт дубликаты UDP/TCP. Работающие линки не закрываются только
|
||||
из-за смены списка адресов. NCD удерживает ссылку на общую запись, пока
|
||||
обслуживает соединение, и использует её при восстановлении.
|
||||
|
||||
`node_conn_direct_open_node` сверяет переданную запись с реестром/БД. Более
|
||||
свежая подписанная запись принимается целиком, старая уступает уже известной.
|
||||
Bootstrap-адреса не перекрывают подписанную запись даже при DOWN.
|
||||
|
||||
Конфигурационные подключения UTUN используют один адаптер
|
||||
`init_client_connections` для старта и reload. Все адреса, TCP и REALITY
|
||||
передаются NCD; адаптер не создаёт ETCP-линки. Новый набор handles открывается
|
||||
до освобождения старого; при ошибке старые handles сохраняются. Конфигурация
|
||||
удерживает один handle на клиента, независимо от количества его адресов.
|
||||
|
||||
Включение конфигурационного пира в UTUN-группу выполняет callback NCD UP.
|
||||
Транспортный UP и наличие callback старого `etcp_connect` больше не включают
|
||||
соединение в UTUN-группу автоматически. Входящий JOIN остаётся явным запросом
|
||||
участия. При пересоздании UTUN-группы учитываются её конфигурационные handles.
|
||||
Удаление клиента отправляет `LEAVE_GROUP` и освобождает удержание UTUN-группы
|
||||
с обеих сторон. Другие владельцы NCD сохраняют транспорт. Поздние NODEINFO и
|
||||
WITHDRAW от отсоединённого UTUN-пира игнорируются, чтобы не вернуть его маршруты.
|
||||
LEAVE ждёт backpressure через queue waiter и временно удерживает собственный
|
||||
NCD handle. Закрытие транспорта отменяет ожидание; новое участие в группе
|
||||
делает ожидающий LEAVE устаревшим, и он не отправляется.
|
||||
Причина удаления пира передаётся явно: только потеря транспорта запускает
|
||||
recovery каскадных маршрутов. Локальный/удалённый LEAVE и удаление битой
|
||||
member-записи не должны восстанавливать намеренно закрытое участие.
|
||||
|
||||
REALITY использует полное SNI до 255 символов; размер поля в записи узла
|
||||
согласован с транспортом. Если локального TCP listen-сокета нет, NCD может
|
||||
создать исходящий TCP-линк с выбором локального адреса операционной системой.
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
Работа лички
|
||||
|
||||
1. отдели логику лички от логики групповых чатов. не смешивай.
|
||||
|
||||
Работа лички. сообщения и медиа доставляется напрямую если узел доступен через bgp.
|
||||
если недоступен - то pending сообщения сохраняются на суперузле и pending media сохраняется на каком-либо медиаузле, а суперузлу сообщается где хранится медиа.
|
||||
при появлении пира суперузел организует доставку медиа до получателя. после успешной доставки медиа/сообщений они удаляются с промежуточных узлов.
|
||||
|
||||
время хранения медиафайлов на промежуточных узлах ограничено - 1 неделя по умолчанию (задачется в конфиге этих узлов).
|
||||
для доставки медиа поднимаем линк (conn_mgr).
|
||||
|
||||
и посмотри реализацию медиа для групповых чатов.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Приглашение в группу через PM
|
||||
|
||||
В desktop chatgui и Android кнопка «+» доступна в строке другого участника
|
||||
и в шапке личной беседы. Она открывает один диалог с фиксированным получателем
|
||||
и выбором группы, в которой отправитель состоит. Группа не выбирается автоматически.
|
||||
|
||||
После нажатия «Пригласить» клиент создаёт свежую ссылку через `invite_build`,
|
||||
ожидает подтверждения регистрации ключа и отправляет PM с `ct="invite"`.
|
||||
Зашифрованное тело: `[group_name:UTF8][NUL][полная ссылка:UTF8]`, без конечного NUL.
|
||||
Снимок для GUI содержит полную ссылку в `data` и название в `tags.group_name`.
|
||||
GUI добавляет заголовок «Приглашение в группу «название»» из перевода интерфейса;
|
||||
язык отправителя не фиксирует язык сообщения у получателя.
|
||||
Успех означает сохранение сообщения и исходящей задачи в одной транзакции.
|
||||
Доставку, повтор и шифрование обеспечивает существующий механизм DM.
|
||||
После успешной отправки открывается личная беседа с получателем.
|
||||
|
||||
Срок действия ключа — **10 минут с момента последней успешной регистрации на каждом узле**.
|
||||
Повторная регистрация того же ключа для того же канала тем же инвайтером продлевает срок.
|
||||
Connection-узел сохраняет ключ при обработке регистрации, инвайтер — после её подтверждения;
|
||||
при совпадении этих ролей запись одна. Для JOIN ключ должен действовать у обоих.
|
||||
Получение PM срок не продлевает. Длительность указана в диалогах отправки и подключения. Поздняя offline-доставка
|
||||
может принести уже просроченную ссылку; в этом случае нужно новое приглашение.
|
||||
Ссылка не содержит время истечения, поэтому клиент показывает длительность,
|
||||
без обратного отсчёта и вычисления оставшегося времени.
|
||||
|
||||
Нажатие на `utun://` в сообщении запускает существующий JOIN и показывает его результат.
|
||||
Если клиент уже состоит в группе, открывается эта группа. Ссылка приглашения отображается
|
||||
одной строкой с многоточием; нажатие и копирование используют полный адрес.
|
||||
В Android долгое нажатие копирует ссылку, в desktop выделение сокращённой строки
|
||||
копирует ссылку целиком без запуска JOIN. Обычные текстовые PM с вручную
|
||||
вставленными ссылками сохраняют текстовое отображение.
|
||||
|
||||
## Контракт операции
|
||||
|
||||
`dm_invite_trampoline` выполняется в uasync-потоке. Запрос содержит `request_id`,
|
||||
ID получателя, выбранную группу и исходную общую группу для создания PM.
|
||||
Из шапки существующей PM исходная группа может быть пустой.
|
||||
|
||||
На instance допускается одно ожидание регистрации приглашения в PM.
|
||||
Повтор во время ожидания возвращает BUSY. Закрытие диалога отменяет только
|
||||
соответствующий `request_id`; уже отправленное сообщение не отзывается.
|
||||
Отмена и остановка DM завершают принятый запрос ровно один раз.
|
||||
|
||||
`CHAT_EVT_DM_INVITE_RESULT` содержит 28 байт:
|
||||
`[request_id:u64][result:i32][peer:u64][conv_id:u64]` в порядке байтов ядра.
|
||||
Desktop и Android сопоставляют результат с запросом и получателем.
|
||||
Ошибки регистрации или сохранения сообщения оставляют диалог открытым для повторной попытки.
|
||||
|
||||
JOIN во вторую группу может использовать уже открытый транспорт общей группы.
|
||||
Поэтому обработчики JOIN_REQUEST и JOIN_READY явно планируют member_sync,
|
||||
а JOIN_READY обновляет кеш каналов до запуска синхронизации.
|
||||
|
||||
## Проверка и диагностика
|
||||
|
||||
- `tests/test_dm_invite`: настоящая UDP-доставка ссылки в другую группу, JOIN,
|
||||
репликация участника и отправка группового сообщения после вступления;
|
||||
отмена, BUSY, неверная группа, rollback при ошибке commit и остановка DM;
|
||||
передача названия с Unicode, кавычками и переводом строки.
|
||||
- `tests/test_chat_join`, `tests/test_chat_join_e2e`, `tests/test_dm_e2e`:
|
||||
существующие сценарии регистрации ключей, JOIN и PM-доставки.
|
||||
- Qt `test_invite_links`: одна строка приглашения на разных ширинах, полный адрес
|
||||
при нажатии и копировании, переносы двух обычных ссылок, их полное содержимое при нажатии,
|
||||
выделение текста и выбор получателя кнопкой в строке участника.
|
||||
- Qt `test_emoji_playback`: перетаскивание ссылки не активирует подключение.
|
||||
|
||||
Для сетевого теста подробности включаются через `UTUN_TEST_DEBUG=1`.
|
||||
В приложении полезны категории `dm=debug`, `chat_sync=debug`, `member_sync=debug`.
|
||||
Операция пишет request, peer, group, результат и TTL; сообщения диагностики
|
||||
новой операции не выводят саму invite-ссылку.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Proxy: потоки TCP и датаграммы
|
||||
|
||||
Клиентские входы — TUN (lwIP), SOCKS5 CONNECT и HTTP proxy/CONNECT. Exit открывает
|
||||
обычный TCP-сокет к назначению. TCP-поток на exit определяется парой `(peer_node_id,
|
||||
stream_id)`. Клиент принимает ответы и уведомления restart только от настроенного
|
||||
exit в UTUN-группе. UDP/ICMP-контексты принадлежат конкретному `UTUN_INSTANCE`.
|
||||
|
||||
## TCP
|
||||
|
||||
Общие определения находятся в `src/proxy/proxy_protocol.h`. Передаваемый заголовок:
|
||||
`svc_id:1, command:1, stream_id:4`; поля маршрута добавляет ETCP-router.
|
||||
|
||||
- CONNECT (1): IPv4:4 и порт:2 в сетевом порядке.
|
||||
- CONNECTED (2): exit подтвердил успешный TCP connect. Только после этого клиент
|
||||
сообщает SOCKS success / HTTP 200 и начинает передавать накопленные данные.
|
||||
- DATA (3): от 1 до 4096 байт.
|
||||
- CLOSE (4), ERROR (5): прекращение потока.
|
||||
- FIN (6): конец одного направления; все предшествующие DATA должны быть переданы.
|
||||
- WINDOW (7): uint32_t, число дополнительно разрешённых байт.
|
||||
|
||||
У каждого направления начальное окно 65536 байт. DATA уменьшает окно отправителя
|
||||
и получателя. Получатель возвращает кредит после записи в локальный TCP-сокет
|
||||
либо принятия данных ограниченным send-buffer lwIP. Очередь ETCP сама по себе
|
||||
не является подтверждением потребления данных конечным TCP.
|
||||
|
||||
FIN ждёт освобождения исходящей очереди, включая данные, остановленные окном
|
||||
или backpressure роутера. Закрытие одного направления не прекращает другое.
|
||||
Окончательное освобождение происходит после обоих FIN и передачи оставшихся данных.
|
||||
Непереданные управляющие сообщения повторяются таймером. Ошибки и переходы
|
||||
состояния диагностируются категорией `proxy`; состояние сокетов — `socket`.
|
||||
|
||||
Парсеры сохраняют остаток после greeting/request/HTTP headers. HTTP CONNECT ждёт
|
||||
полного блока заголовков. Во время DNS/connect чтение ограничено очередью tcp_io;
|
||||
накопленные HTTP headers/body передаются частями размером DATA.
|
||||
|
||||
## UDP и ICMP
|
||||
|
||||
REQUEST и REPLY различаются явно. Ответ допускается только от настроенного exit.
|
||||
UDP-ответ восстанавливается с нулевой UDP checksum (допустимо для IPv4), IPv4 checksum
|
||||
пересчитывается. ICMP checksum учитывает нечётный последний байт без чтения за буфером.
|
||||
|
||||
Exit заменяет ICMP id/sequence уникальной среди ожидающих запросов парой и сопоставляет
|
||||
ответ также с IP назначения. Исходные id/sequence восстанавливаются для клиента.
|
||||
Это исключает смешивание ping-запросов разных клиентов с одинаковыми исходными ID.
|
||||
|
||||
## Проверки и ограничения
|
||||
|
||||
`test_proxy_packets` проверяет IPv4/UDP/ICMP и payload длиной 0..1501.
|
||||
`test_tcp_io_flush` проверяет уведомление после последнего синхронного send.
|
||||
`test_proxy_regressions` использует реальные локальные сокеты и управляемую доставку
|
||||
ETCP: совместные/раздельные handshake, ранние данные, отложенный CONNECTED, HTTP 502,
|
||||
большой POST, одинаковые stream ID разных пиров, FIN при закрытом окне, RST и IPv4 options.
|
||||
|
||||
На Linux полный `./check.sh`: 109 passed, 0 failed, 1 skipped; дополнительно прошли
|
||||
настоящий TUN, full-duplex burst и нагрузка 8 соединений / 128 МиБ.
|
||||
Windows/FreeBSD в этом прогоне не проверялись.
|
||||
|
||||
SOCKS IPv6 явно отклоняется кодом address type not supported. IPv4-фрагменты
|
||||
на входе proxy TUN отклоняются с диагностикой; сборка фрагментов не реализована.
|
||||
Формат TCP дополнен CONNECTED/WINDOW, UDP-команды разделены: клиент и exit необходимо
|
||||
обновлять вместе; совместимость со старым proxy-протоколом не предусмотрена.
|
||||
@@ -0,0 +1,337 @@
|
||||
# Жизненный цикл ядра и сервисов
|
||||
|
||||
CLI выполняет daemon fork до создания `UASYNC`, SQLite, сокетов и фоновых потоков.
|
||||
Сервисы и их workers создаются уже в дочернем процессе. Родитель ждёт подтверждения
|
||||
успешной инициализации и записи PID-файла; ошибка старта возвращается вызывающему.
|
||||
`test_daemon_worker` проверяет настоящий запуск без `-f`, наличие дискового потока,
|
||||
штатный SIGTERM и ненулевой статус launcher при ошибке PID-файла.
|
||||
|
||||
Все операции выполняются в потоке единственного `UASYNC` экземпляра, вне callbacks
|
||||
останавливаемых модулей. GUI передаёт команды в этот поток.
|
||||
|
||||
```c
|
||||
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config);
|
||||
if (!inst || utun_core_start(inst) < 0) /* обработать ошибку */;
|
||||
if (chat_service_start(inst) < 0) /* обработать ошибку */;
|
||||
if (utun_service_start(inst) < 0) /* обработать ошибку */;
|
||||
|
||||
chat_service_stop(inst); /* UTUN продолжает работать */
|
||||
chat_service_start(inst); /* загружает сохранённые каналы */
|
||||
utun_service_stop(inst); /* чат продолжает работать */
|
||||
utun_instance_destroy(inst);
|
||||
```
|
||||
|
||||
`utun_instance_init()` — сокращение для запуска ядра, UTUN и чата, если чат включён
|
||||
в конфигурации. Desktop GUI запускает ядро, UTUN и чат явно; TUN создаётся при
|
||||
`[global] tun_enabled=yes`. Без TUN и NAT запуск чата не требует прав администратора.
|
||||
Desktop подтверждает start только после готовности всех сервисов и uasync poll;
|
||||
ошибка TUN откатывает запуск и возвращается в GUI. Android запускает ядро и чат без UTUN.
|
||||
Повторный start работающего сервиса и повторный stop допустимы. Ошибка старта
|
||||
сервиса откатывает созданные им ресурсы. `utun_instance_stop()` останавливает цикл
|
||||
приложения; освобождение ядра выполняет `utun_instance_destroy()`.
|
||||
|
||||
Для TUN в desktop `vibechat.cfg` задаются те же параметры, что и у CLI:
|
||||
|
||||
```ini
|
||||
[global]
|
||||
tun_enabled=yes
|
||||
tun_ifname=vibechat
|
||||
tun_ip=10.42.0.1
|
||||
|
||||
[routing]
|
||||
my_subnet=10.42.0.1/32
|
||||
route_subnet=10.42.0.0/24
|
||||
```
|
||||
|
||||
Адреса и маршруты выбираются под свою сеть; изменения применяются после перезапуска.
|
||||
На Windows нужна `wintun.dll` рядом с `vibechat.exe` и запуск с правами администратора.
|
||||
CMake копирует DLL и лицензии из `lib/`; Windows-упаковка явно включает DLL, проверяет
|
||||
её архитектуру x64 и зависимости, поскольку `LoadLibrary` не отражается в импортах EXE.
|
||||
На Linux необходимы доступ к `/dev/net/tun` и права на настройку интерфейса и маршрутов.
|
||||
|
||||
Успех запуска требует успешной инициализации всех обязательных ресурсов: заданных
|
||||
listen-сокетов, мониторинга, TUN и всех `route_subnet` при включённом TUN. Частичный
|
||||
набор сокетов или маршрутов — ошибка запуска. `utun_started` устанавливается только
|
||||
после завершения всех шагов. Системные маршруты хранятся отдельным списком успешно
|
||||
созданных записей; stop и откат удаляют только этот список, в обратном порядке.
|
||||
Маршрут, на добавлении которого ядро вернуло ошибку, не становится собственностью
|
||||
UTUN и при откате не удаляется. Ошибка одного удаления не прекращает очистку остальных.
|
||||
|
||||
`auto_sockets=android` выбирает внешнее управление сетью: frontend регистрирует
|
||||
ConnectivityManager до запроса запуска и передаёт конфигурацию сокетов через JNI.
|
||||
Оба нативных монитора ОС в этом режиме не создаются. Рабочие listen-сокеты по-прежнему
|
||||
должны открыться полностью. Android подтверждает запуск по `SERVICE_STARTED`, а не
|
||||
по успешному созданию потока; после события передаётся свежий снимок адресов.
|
||||
|
||||
| Владелец | Ресурсы |
|
||||
|---|---|
|
||||
| Ядро | Идентичность узла, реестр узлов и групп, SQLite, транспортные сокеты, NCD, маршрутизатор, общая репликация, время, мониторинг |
|
||||
| UTUN | Группа UTUN, запросы соединений из конфигурации, DATA binding, TUN, системные маршруты, NAT transport |
|
||||
| Чат | CHAT-группы, запросы подключения и join, экземпляры синхронизации каналов, chat API, медиа, личные сообщения, звонки и headless API |
|
||||
| Группа | Сессии пиров, её маршруты и логические каналы, планировщик подключения и recovery |
|
||||
| Сессия пира группы | NCD handle с момента CONNECTING, согласованные эпохи, состояние обмена таблицей, очередь отправки |
|
||||
|
||||
SQLite открывается ядром один раз: `db_path/chats.db`, либо `:memory:`, если путь
|
||||
не задан. Остановка чата сохраняет каналы и сообщения в этой БД. В варианте
|
||||
`:memory:` данные сохраняются только до уничтожения ядра.
|
||||
|
||||
Работающий общий транспорт сохраняется, пока он нужен хотя бы одному из пиров. Закрытие запроса одной группы
|
||||
не разрывает соединение, нужное другой группе. При остановке группы отменяются
|
||||
её планировщики, запросы, таймеры и ожидания очередей; отправляется LEAVE,
|
||||
закрываются логические каналы маршрутизатора и удаляется её ожидающий транзит
|
||||
на общих соединениях. Транзит других групп сохраняется. Отложенный LEAVE может временно
|
||||
удерживать транспорт самостоятельно, не обращаясь к уже удалённой группе.
|
||||
|
||||
ETCP удаляет транспорт после 30 секунд непрерывного DOWN только при отсутствии локальных NCD handles.
|
||||
Первоначальное ожидание UP тоже учитывает владение. Повторный DOWN не продлевает
|
||||
таймаут; восстановление UP отменяет его. Если срок истёк при наличии handles, транспорт
|
||||
сохраняется, а исходный deadline остаётся. Освобождение последнего handle во время DOWN
|
||||
оставляет транспорт до этого deadline либо запускает удаление в следующем callback,
|
||||
если срок уже истёк. Внешние ETCP refs задерживают только освобождение памяти.
|
||||
Явный force-close и отмена неудачной попытки последнего владельца закрывают сразу.
|
||||
|
||||
Входящий UP без локальных handles сохраняется: он может быть нужен пиру. CLOSE/KEEP_ALIVE
|
||||
привязан при запуске ядра, поэтому мягкое закрытие работает и на чисто входящем узле.
|
||||
На CLOSE при наличии handles отвечаем KEEP_ALIVE; без них закрываем транспорт.
|
||||
KEEP_ALIVE полностью отменяет FIN_WAIT. Ожидание CLOSE обычно занимает 5 секунд,
|
||||
для спящего пира увеличивается до его RX deadline. Сам факт DOWN во время FIN_WAIT
|
||||
завершает ранее запрошенное мягкое закрытие.
|
||||
|
||||
Контроль RX независим от отправки keepalive. Объявленный сон не отключает проверку
|
||||
жизни: срок от последнего RX равен максимуму согласованного keepalive timeout и
|
||||
объявленного остатка ACTIVE+SLEEP с запасом normal timeout. Фазовые таймеры и наши
|
||||
отправки срок жизни пира не продлевают. Пробуждение сразу сокращает deadline до normal timeout.
|
||||
После истечения последнего линка транспорт
|
||||
становится DOWN, затем может удаляться через 30 секунд, когда локальных NCD handles нет. `test_etcp_server_lifecycle`
|
||||
проверяет сроки, несколько путей, восстановление, владение, сон и CLOSE/KEEP_ALIVE
|
||||
с управляемыми часами и контролем освобождения памяти/таймеров.
|
||||
|
||||
Остановка чата сначала прекращает внешние команды и фоновые задания, затем
|
||||
закрывает медиа и сервисы, синхронизацию и группы, после чего освобождает chat core.
|
||||
`media_async_destroy()` дожидается завершения native workers и вызывает каждый
|
||||
ожидающий callback с `MEDIA_ASYNC_CANCELLED`, пока его контекст ещё существует.
|
||||
Worker резервирует completion до запуска и передаёт его через `uasync_post_reserved`;
|
||||
периодического polling нет. Destroy выполняется вне callbacks uasync, после join
|
||||
отменяет pending post и завершает done ровно один раз. Uasync живёт до конца этой очистки.
|
||||
Результаты отменённых задач не записываются в БД. Это синхронное ожидание: длительное
|
||||
транскодирование или распознавание речи может задержать stop; принудительного
|
||||
прерывания этих библиотек нет.
|
||||
|
||||
Владелец принимает не более 16 задач, включая уже завершённые workers до доставки
|
||||
completion. Отказ запуска/неверные аргументы дают синхронный done с -1, остановка
|
||||
владельца — с `MEDIA_ASYNC_CANCELLED`; при NULL done только логируется ошибка.
|
||||
Примитивы `media_async.h` синхронные: отмена задачи не откатывает выполненные ими
|
||||
действия. `ma_copy_file()` публикует копию внутри worker после проверенных записи и
|
||||
закрытия temp; вызывающий отвечает за очистку своего результата при отмене.
|
||||
Подробные файловые контракты, 64-битные размеры и ошибки RNG описаны в `media_async.h`.
|
||||
|
||||
Подготовка файлов в `src/video/video.h` выполняет remux подходящих H.264/AAC либо
|
||||
перекодирование в MP4 H.264/YUV420P и AAC. Worker пишет уникальный соседний временный
|
||||
файл, завершает декодеры/ресемплер/FIFO/кодеры, проверяет trailer, закрытие и метаданные.
|
||||
Конечное имя заменяется только успешным completion в uasync. При отмене, включая
|
||||
отмену уже подготовленного результата до доставки completion, временный файл удаляется,
|
||||
старое назначение сохраняется. Совпадающие исходник/назначение и файловые алиасы
|
||||
отклоняются; исходный файл должен оставаться неизменным до done. В процессе занята
|
||||
одна задача до completion; атомарный захват исключает гонку между экземплярами ядра.
|
||||
Перед done занятость освобождается, поэтому callback может запустить следующую задачу.
|
||||
JPEG использует те же правила файлов, но извлекается синхронно в отдельном frontend worker.
|
||||
Probe также синхронен и не обещает ограниченного времени чтения.
|
||||
|
||||
Live media хранится в RS-кольце с общим индексом PTS; каждый локальный handle владеет
|
||||
отдельным cursor. Пауза player не останавливает сетевой ingest и не закрепляет старые
|
||||
части против вытеснения. После FIN publisher может снять своё владение, сохранив
|
||||
завершённый источник для оставшихся локальных readers. Последний close освобождает
|
||||
источник, индекс и кольцо. Новая публикация отсоединяет readers прежней эпохи.
|
||||
Android preview собственного эфира не создаёт reader: Camera2 пишет прямо в TextureView
|
||||
и encoder Surface. UI attach/detach меняет лишь camera session; audio/encoder/origin сохраняются.
|
||||
UI, ожидающие команды camera handler и каждая camera session удерживают preview target отдельно.
|
||||
TextureView возвращает false при destroy и снимает ссылку UI; Surface/SurfaceTexture освобождаются
|
||||
после закрытия последней session. CameraDevice.onClosed снимает также оставшиеся ссылки своих
|
||||
sessions, если Android не прислал отдельные session.onClosed. Повторные callbacks безопасны.
|
||||
Capture stop запрещает attach и снимает своё владение target; camera HandlerThread завершается
|
||||
после camera close и освобождения outputs всех созданных sessions.
|
||||
Отказ preview output возвращает encoder-only session и не завершает сам эфир.
|
||||
При stop/destroy отменяются delivery и media-index callbacks до освобождения store;
|
||||
destroy отсоединённого cursor не обращается к store. Frontend прекращает audio callback
|
||||
и worker до освобождения своего DSP; borrowed handle/RS pointers между потоками не передаются.
|
||||
|
||||
Чтение video cursor запрашивает следующий кадр по готовности decoder, независимо от
|
||||
PTS показа; audio cursor сохраняет временной lookahead. Viewers готовят ограниченный батч
|
||||
на запрос: отдельно задаются число видеокадров, audio lookahead и число всех записей.
|
||||
Desktop и Android принимают до 16 записей и обрабатывают треки по их готовности.
|
||||
VIDEO_END/AUDIO_END следуют за последними пакетами своего трека, не ожидая другого трека;
|
||||
общий END завершает вход, а окно ждёт опустошения обоих выходов. GAP/CLOSED отменяют старый батч.
|
||||
Получение батча погашает запрос, включая повторные обращения до pull; скрытого чтения следующего батча нет.
|
||||
Callback готовности только будит desktop worker и не вызывает API ядра/bridge. Stop снимает callback
|
||||
под mutex viewer до освобождения его контекста; detach будит ожидающие workers до уничтожения manager.
|
||||
На Android signal принадлежит audio worker и живёт до stream_stop, join video worker и close mixer input;
|
||||
ядро только уведомляет его. Surface освобождается после join video worker.
|
||||
В wait не удерживаются mutex bridge/ядра. Расход PCM будит тот же signal, уведомления сливаются без потери пробуждения.
|
||||
GUI и audio callback будят worker при движении часов/освобождении места; сна между готовыми пакетами нет.
|
||||
Каждый reader имеет отдельные audio/control и video workers. Audio worker читает батчи и готовит
|
||||
до 250ms PCM через Opus/SoundTouch, не удерживая блокировку consumer. Запас пополняется при наличии
|
||||
данных без обязательного prefill. SPSC передаёт PCM вместе с отображением исходных PTS; mixer/audio
|
||||
callback только копирует и смешивает. Короткая блокировка защищает output map/clock и lifetime,
|
||||
в ней нет декодирования, time-stretch или MediaCodec/FFmpeg. EOF продолжает prepare до опустошения DSP.
|
||||
Android video worker получает ограниченный mailbox (два кадра) и держит один рабочий input и один готовый output MediaCodec; свободный
|
||||
input buffer получает лишь для уже готовых данных и сразу возвращает codec.
|
||||
Первый decoded frame разрешает движение clock/audio. Будущий output ждёт PTS и
|
||||
приостанавливает новые video requests, сохраняя независимую обработку audio/control.
|
||||
Если первый доступный звук позже KEY, cursor отсчитывает стартовый audio lookahead от
|
||||
первого аудиопакета, сохраняя это основание неизменным. Opus/SoundTouch получает PCM до
|
||||
запуска audio clock; неподвижные часы не разрешают читать дальше этого ограниченного окна.
|
||||
PTS поданных кадров служат диагностике/проверке запаздывания, а не лимиту заполнения codec.
|
||||
Смена revision заменяет только video decoder и его диагностические измерения;
|
||||
GAP/EXPIRED начинают новую точку KEY. Нехватка сети сохраняет decoder references.
|
||||
Reset увеличивает video generation; результаты прежнего поколения не публикуются. Video worker
|
||||
после EOF остаётся доступен для reset до явного stop. Audio clock после старта не ждёт видео:
|
||||
просроченные изображения декодируются для сохранения references, но не показываются.
|
||||
Desktop FFmpeg держит один рабочий AVPacket и два готовых изображения; EAGAIN оставляет
|
||||
packet до следующего прохода. FIN подаёт EOS и дренирует выход до decoder EOF;
|
||||
конец GUI требует также показа оставшихся изображений и завершения audio tail.
|
||||
Отсутствие прогресса decoder при наличии работы имеет отдельную диагностику и срок 10s.
|
||||
|
||||
ACK-история принадлежит ядру и обновляется только при первом найденном подтверждении
|
||||
пакета. REINIT/close соединения не стирает уже подтверждённые за последнюю секунду байты.
|
||||
Общий `stream_publisher` Android/desktop владеет отдельным 100ms таймером TX-снимков;
|
||||
close/detach отменяют его в uasync до уничтожения instance. Bridges копируют снимок
|
||||
под mutex, frontend не обращается к живым очередям транспорта. Android экран подписан
|
||||
на отдельный StateFlow статистики; desktop обновляет статистику отдельно от сырого превью.
|
||||
Publisher не регистрирует stream event callback и не подменяет callback viewer.
|
||||
Bridge удерживает внешний mutex на время producer/open/stop и destroy общего publisher;
|
||||
actor не вызывает frontend callbacks под своим mutex. Reserved post, media timer,
|
||||
stats timer и stream handle освобождаются до контекста publisher.
|
||||
|
||||
Desktop `MainWindow` владеет `StreamPublishEngine`, а окно управления лишь отображает
|
||||
его состояние. Закрытие окна сохраняет эфир, новый запуск запрещён до конца FIN drain.
|
||||
Qt capture callback сохраняет последний `QVideoFrame` для preview и ограниченно ставит кадр
|
||||
в очередь отдельного долгоживущего image worker. Он конвертирует `QVideoFrame` в `QImage`;
|
||||
video worker получает готовый image и не создаёт Qt Multimedia TLS, которое могло задержать его exit на Windows.
|
||||
Video/audio workers выполняют encode/DSP вне GUI и hardware callbacks. Отдельные поколения источника и encoder позволяют менять
|
||||
видеокодек/bitrate/rotation без повторного выбора экрана в портале. Ошибки Qt capture
|
||||
обрабатываются через queued callback, чтобы не уничтожать backend из собственного callback.
|
||||
Stop запрещает producers, закрывает Qt capture и подписку на аудиозахват. Отдельный join worker
|
||||
дожидается полного выхода video/audio threads, включая native TLS Qt; GUI продолжает обрабатывать события.
|
||||
После join отправляется FIN с drain 2s; закрытие последнего микрофона может продолжаться параллельно.
|
||||
Microphone lease и owner сохраняются до завершения обоих путей. Новый start ждёт также FIN drain.
|
||||
При завершении приложения engine уничтожается до SoundManager и ядра; detach в uasync
|
||||
закрывает оставшийся publisher. Destructor ждёт workers в локальном event loop без пользовательского ввода,
|
||||
чтобы thread cleanup мог обратиться к GUI; start и внешние сигналы engine в это время запрещены.
|
||||
Workers никогда не держат borrowed instance/handle.
|
||||
Microphone lease публикации исключает call/recorder; рация согласует вход через `AudioCapture`.
|
||||
Одинаковый выбранный вход имеет одну аппаратную сессию и профиль трансляции. Разные входы независимы.
|
||||
Подключение/отключение рации не останавливает устройство трансляции: атомарный слот и счётчик readers
|
||||
дожидаются только callback удаляемого потребителя. Последняя подписка закрывает устройство.
|
||||
Если первой работала рация, публикация заменяет профиль захвата своим; после остановки публикации
|
||||
оставшаяся рация продолжает тот же захват. Recovery останавливает callbacks, workers TX и один раз
|
||||
открывает общий вход; RX рации сохраняется. Каждый потребитель обрабатывает собственную копию PCM.
|
||||
На Android `MediaAudioController` владеет общим выбором устройства, AudioManager и одним аппаратным reader.
|
||||
Маршрутизация использует `CallAudioRouter`; AudioTrack рации и микшера просмотра регистрируются у одного владельца.
|
||||
`MicrophoneCapture` раздаёт независимые ограниченные очереди постоянного PCM 48k stereo; моно апмиксуется.
|
||||
После подключения публикации встроенный микрофон использует CAMCORDER, сохраняемый до последней подписки.
|
||||
Смена профиля заменяет только аппаратный AudioRecord: камера, видеокодер и source_epoch продолжают работать.
|
||||
Общая сетка по capture timestamps имеет запас 200ms; пропуски заполняются тишиной, старый PCM не переносится вперёд.
|
||||
Stop публикации удаляет её подписку до join Opus worker и FIN, сохраняя захват для оставшейся рации.
|
||||
PTT и PCM рации проходят через одну очередь. Последняя подписка завершает общий reader;
|
||||
контроллер закрывает аппаратный вход, сохраняя выходы. Перед Telecom все владельцы снимаются,
|
||||
а `MediaAudioController.awaitIdle()` дожидается освобождения входа и глобального маршрута.
|
||||
Ограничения фонового захвата рации, её разрешения и foreground-сервис продолжают действовать.
|
||||
Fatal ошибка audio worker передаётся в GUI с session ID и завершает всю публикацию;
|
||||
откат видеоконфигурации применяется только к video/capture. Поздний отказ старого publisher
|
||||
не завершает новую сессию. Причина ошибки сохраняется в окне после окончания drain.
|
||||
|
||||
Desktop-звонок и рация проверяют поступление корректного PCM, включая первый callback.
|
||||
Секунда без данных отключает TX и фиксирует понятную ошибку в GUI; цифровая тишина является данными.
|
||||
RX продолжает работать, рация снимает PTT, автоматического переоткрытия неисправного входа нет.
|
||||
Выбор другого микрофона или новая сессия снимают ошибку. Неисправное устройство закрывается
|
||||
в отдельном потоке; stop не присоединяет уже запущенный close worker. Новый захват ждёт его завершения.
|
||||
В звонке microphone lease удерживается до конца закрытия даже после stop. Общий AudioCapture
|
||||
рации сначала отсоединяет callback потребителя; последний владелец освобождает backend в worker.
|
||||
Сброс контекста и уничтожение владельца дожидаются worker до освобождения miniaudio-контекста.
|
||||
Штатное закрытие исправного устройства сохраняет синхронный порядок.
|
||||
|
||||
`AudioDevice` подстраивает буфер по подтверждённым backend XRUN: WASAPI DATA_DISCONTINUITY
|
||||
на входе, ALSA EPIPE, PulseAudio capture hole/overflow и playback underflow. Notification
|
||||
только увеличивает атомарный счётчик, решение и лог выполняются в GUI recovery timer.
|
||||
Исходные параметры, включая Linux capture period=10мс, сохраняются до повторных сбоев.
|
||||
Первая секунда после start исключена; нужны ошибки в двух опросах за 5с. Период увеличивается
|
||||
на 5мс, не чаще раза в 5с, до общей запрошенной ёмкости 100мс с учётом числа периодов.
|
||||
Фактический размер согласует backend; исходный или уже достигнутый больший размер не уменьшается.
|
||||
При достижении предела повторные сбои логируются без новых перезапусков. Подобранный запрос
|
||||
сохраняется при recovery, сбрасывается при смене устройства/сессии; стабильный звук не вызывает reopen.
|
||||
Подстройка микрофона закрывает устройство асинхронно, останавливая TX worker и сохраняя RX.
|
||||
Общая capture-сессия отсоединяет callbacks до close и заново подготавливает потребителей после init.
|
||||
Новая подписка во время close откладывается, stop удаляет её без ожидания backend.
|
||||
Первое переоткрытие после подстройки не использует backoff; при ошибке открытия действует обычный retry.
|
||||
|
||||
В desktop-видеозвонке Windows/Linux камера живёт в отдельном процессе того же
|
||||
`vibechat --call-camera-worker`. Служебный вход выполняется до конфига, БД и
|
||||
single-instance lock. Перечисление устройств, Qt/V4L2, преобразование и освобождение
|
||||
аппаратных кадров происходят только там. GUI получает собственный `QImage`; TX не
|
||||
обращается к драйверу. Смена codec не перезапускает захват и не ждёт следующего GOP.
|
||||
|
||||
Пиксели одного кадра передаются через mapping (Windows — pagefile-backed объект ОС,
|
||||
Linux — временный файл), метаданные/команды — через локальный сокет.
|
||||
Worker записывает mapping до FRAME; следующий write разрешён
|
||||
только после ACK, который GUI посылает после копирования в свою память. Общего mutex
|
||||
нет. В worker ожидает только последний raw-кадр; поколения отсекают старые результаты.
|
||||
Preview обновляется по готовности, TX получает condition-variable wake. PTS сохраняет
|
||||
время захвата; кадры старше 250мс отбрасываются до encode и перед send. Отброшенный
|
||||
encoded frame требует следующего KEY, поскольку encoder уже продвинул reference chain.
|
||||
|
||||
Отключение камеры и удаление окна не ждут worker. Независимый Session сохраняет
|
||||
QProcess и mapping до finished, затем удаляет себя после отсоединения владельца.
|
||||
Зависший stop получает kill через 1.5с, отсутствие прогресса — через 5с; таймеры
|
||||
живут в родительском процессе. При выходе приложения kill отправляется без wait;
|
||||
GUI не уничтожает живой QProcess. При отказе новый процесс запускается лишь по
|
||||
явной команде пользователя и после finished прежнего. Лог категории call показывает
|
||||
последнюю стадию enumerate/start/convert/release/stop и возраст кадра.
|
||||
`test_call_camera_capture` проверяет настоящую IPC-цепочку с подставным устройством:
|
||||
блокировки start/map/stop, поздние кадры после смены поколения, задержку 720p,
|
||||
повтор после отказа, отсутствие рестарта камеры при смене codec, удаление окна,
|
||||
выход приложения и независимое освобождение mapping в каждом процессе.
|
||||
|
||||
Статус транспорта UP подтверждает восстановление физического пути. READY сессии
|
||||
группы означает согласованный JOIN/ACCEPT и завершённый обмен её таблицей; именно
|
||||
его ждёт recovery. REINIT меняет эпохи обмена, сохраняя рабочие маршруты. Эпохи
|
||||
защищают от сообщений старой сессии и не задают срок жизни маршрута.
|
||||
|
||||
Проверки: `test_services` проверяет независимый stop/start, общий NCD в CONNECTING,
|
||||
сохранение идентичности/сокетов/БД, повторную загрузку каналов, отмену workers,
|
||||
заблокированного медиа-потока, транзита и ожиданий заменённой очереди. `test_chat_join_e2e`
|
||||
проверяет join и распространение membership между узлами без UTUN-сервиса.
|
||||
|
||||
В Android звонковой сессией владеет `CallController` уровня приложения; события
|
||||
ядра и команды Telecom обрабатываются один раз в main thread, без повторного
|
||||
проигрывания событий при создании ViewModel. UI получает `StateFlow` и передаёт
|
||||
пользовательские команды. `CallService` владеет `CallAudioEngine` и частичным
|
||||
wake lock: перед исходящим вызовом или принятием входящего запускается foreground
|
||||
режим `phoneCall|microphone`, затем сигналинг и аудио. Завершение сначала
|
||||
останавливает I/O и дожидается аудиопотоков, затем освобождает native-аудио,
|
||||
wake lock и сервис. Перед `NativeLib.stop/restart/destroy` выполняется тот же
|
||||
порядок; новый звонок разрешается после `SERVICE_STARTED` нового ядра.
|
||||
|
||||
Активность ядра задаётся явно: видимая Activity или незавершённый звонок удерживают
|
||||
ACTIVE и выключают Android standby. Состояние не истекает по таймеру. После
|
||||
завершения звонка в фоне возвращается standby; экран может гаснуть во время
|
||||
разговора независимо от частичного wake lock сервиса.
|
||||
|
||||
Если работают рация, публикация или просмотр трансляции, исходящий Android-звонок
|
||||
требует подтверждения их остановки; входящий показывает предупреждение до принятия.
|
||||
После подтверждения/принятия `CallController` выключает участие в рации, завершает
|
||||
публикацию и просмотр, асинхронно ждёт освобождения их аудио и только затем запускает
|
||||
сигналинг и звонковое аудио. Foreground-сервис резервируется сразу из видимой Activity.
|
||||
Отмена во время освобождения не запускает устаревший звонок.
|
||||
До окончания звонка повторное включение рации/трансляции заблокировано.
|
||||
После закрытия звонкового аудио и Telecom Connection ранее включённая рация
|
||||
возвращается в прежний канал; удержания PTT сброшены. Удалённый канал и остановленное
|
||||
ядро не восстанавливаются. Публикация и просмотр трансляции автоматически не возобновляются.
|
||||
|
||||
`utun_set_client_activity()` в сборке с `UTUN_HAVE_STANDBY` применяет фактический
|
||||
режим duty-cycle до рассылки событий активности; JNI не переключает его отдельно.
|
||||
Повторная команда не перезапускает фазу и анонсы, но первый фоновый запуск включает
|
||||
standby даже при начальном `client_activity=STANDBY`. Keepalive подписан на режим
|
||||
и фазы самого standby; новые UDP/TCP-линки сразу анонсируют текущую фазу после
|
||||
handshake. Пробуждение по таймеру и внешней сети сначала сообщает ACTIVE-окно
|
||||
пирам, затем запускает ожидающие задачи. При выходе сначала восстанавливаются
|
||||
линки; deinit отменяет ожидающие задачи и очищает подписки старого ядра.
|
||||
+1295
File diff suppressed because it is too large.
Load diff
+104
@@ -0,0 +1,104 @@
|
||||
# Независимая STUN-диагностика NAT
|
||||
|
||||
Модуль `src/transport_layer/stun_nat.c/h` измеряет NAT на уникальных путях существующих IPv4 UDP-сокетов.
|
||||
Ключ группы — семейство, фактический IP и netif; имена и рабочие порты не участвуют.
|
||||
Для каждой группы создаётся один собственный неблокирующий UDP-сокет с тем же локальным IP,
|
||||
привязкой к устройству и Linux SO_MARK / FreeBSD FIB. Тестовый порт выбирается случайно из 49152–65535.
|
||||
Модуль сам регистрирует чтение в uasync, принимает STUN и закрывает свой сокет после измерения.
|
||||
STUN не меняет исходный ETCP fd, его порт и обработчики. Через `stun_nat` API модуль
|
||||
`nat_detection` получает предварительную оценку для рабочих сокетов того же IP/netif
|
||||
с совпадающими mark/FIB/device. Если два разных IP видят одинаковый стабильный внешний
|
||||
IP:порт и есть трансляция адреса, рабочий профиль получает EIM? с source=STUN и inbound=UNKNOWN.
|
||||
Это действует и для обычных серверов без запроса с незнакомого IP. Filtering тестового порта
|
||||
не переносится; измерение mapping рабочего сокета всегда имеет приоритет.
|
||||
Оценка снимается после TTL, отключения STUN, смены пути или противоречащего внешнего IP.
|
||||
Тестовый mapped endpoint не копируется в `nat_addr` и NODEINFO: адрес для подключения
|
||||
должен быть получен наблюдением самого рабочего сокета. Детектор описан в `doc/nat_detection.txt`.
|
||||
|
||||
## Конфигурация
|
||||
|
||||
```ini
|
||||
[stun]
|
||||
enabled=yes
|
||||
server=stun-a.example.org:3478
|
||||
server=stun-b.example.org:3478
|
||||
# Дополнительные строки — резервные серверы.
|
||||
|
||||
[debug]
|
||||
nat=debug
|
||||
```
|
||||
|
||||
Без `enabled=yes` модуль выключен. Без порта используется 3478. Список сохраняет порядок конфига.
|
||||
При включении нужны минимум две строки `server`; для измерения выбираются два ответивших сервера с различными IP.
|
||||
Совпадающие IP после DNS пропускаются. DNS асинхронный, с использованием существующего IPv4 A-резолвера.
|
||||
IPv6, SOCKS UDP и `only_local` пропускаются с явным статусом; TCP не проверяется.
|
||||
|
||||
## Результат
|
||||
|
||||
`stun_nat_get_result(instance, "имя исходного сокета", &result)` копирует локальный снимок:
|
||||
|
||||
Имя сначала разрешается в текущий рабочий сокет, затем в группу. API ядра
|
||||
`stun_nat_get_socket_result(instance, socket, &result)` дополнительно проверяет
|
||||
текущие IP/netif и параметры маршрутизации. Несколько рабочих портов получают общий снимок.
|
||||
|
||||
- `local` — тестовый IP:port; `servers[2]` и `mapped[2]` — реальные серверы и наблюдавшиеся внешние адреса;
|
||||
- `translated`: -1 неизвестно, 0 трансляция не наблюдалась, 1 внешний адрес отличается;
|
||||
- `mapping`: UNKNOWN, MATCH, DEPENDENT, EIM, ADM, APDM;
|
||||
- `filtering`: UNKNOWN, EIF, ADF, APDF;
|
||||
- причины неполного результата, статус, время измерения и обнаруженная поддержка RFC 5780.
|
||||
- ключ группы, generation, evidence_id, measured_at_us, количество рабочих сокетов и признак refresh.
|
||||
|
||||
Обычный STUN даёт MATCH при одинаковых адресах и DEPENDENT при разных. MATCH не доказывает EIM,
|
||||
а обычные Binding-ответы не позволяют определить filtering. Первоначальный сервер опрашивается повторно:
|
||||
смена его mapping во время проверки инвалидирует классификацию как нестабильную.
|
||||
|
||||
## RFC 5780
|
||||
|
||||
Корректные OTHER-ADDRESS и RESPONSE-ORIGIN включают автоматическую попытку Behavior Discovery.
|
||||
Нужны две публичные IP и два порта на серверной стороне, доступные во всех четырёх комбинациях.
|
||||
|
||||
1. Обычный Binding получает внешний адрес и альтернативный endpoint.
|
||||
2. CHANGE-REQUEST с флагами IP+port проверяет EIF.
|
||||
3. Если ответ не пришёл, контрольный Binding проверяет живость сервера и стабильность mapping.
|
||||
4. CHANGE-REQUEST только port различает ADF и APDF; после таймаута снова выполняется контрольный Binding.
|
||||
5. Перед публикацией ADF/APDF альтернативные endpoints проверяются обычными Binding-запросами.
|
||||
Недоступный альтернативный endpoint даёт UNKNOWN, а не ложную строгую фильтрацию.
|
||||
6. После filtering проверяется mapping: альтернативный IP с первоначальным портом, затем при необходимости
|
||||
альтернативный IP с альтернативным портом. Сравнение различает EIM, ADM и APDM.
|
||||
7. Второй сервер и повтор первого проверяют независимую пару и стабильность результата.
|
||||
|
||||
Все filtering-тесты выполняются до открывающих фильтр отправок на альтернативные endpoints.
|
||||
Собственные предыдущие STUN-отправки учитываются: при пересечении адресов следующего сервера
|
||||
сомнительный тест filtering пропускается. ERROR 420 и игнорирование CHANGE-REQUEST дают UNKNOWN.
|
||||
Аутентификация STUN не реализована: требующий её сервер фиксируется как ошибка, затем используется резервный.
|
||||
|
||||
Запрос содержит случайный 96-битный transaction ID и FINGERPRINT. Проверяются источник ответа,
|
||||
transaction ID, границы/выравнивание атрибутов, XOR-MAPPED-ADDRESS, RESPONSE-ORIGIN и FINGERPRINT при его наличии.
|
||||
Повторы одной транзакции сохраняют ID: RTO от 500 мс удваивается, всего семь отправок, последнее ожидание 8 секунд.
|
||||
Новые транзакции всех проб ограничены десятью в секунду; ожидания не блокируют event loop.
|
||||
|
||||
## Жизненный цикл и проверки
|
||||
|
||||
Ядро запускает модуль при `utun_core_start`; остановка уничтожает его до сетевых ресурсов и config.
|
||||
Перезагрузка конфига отменяет старые DNS/таймеры и создаёт новый модуль.
|
||||
Раз в три секунды выполняется локальная сверка групп: новые/изменённые проверяются,
|
||||
группа удаляется после исчезновения последнего рабочего сокета. Смена имени/представителя
|
||||
или удаление дубликата не теряет кэш. Несовпадение mark/FIB/device внутри одного ключа
|
||||
даёт UNKNOWN с причиной incompatible routing policy.
|
||||
TTL результата 300 секунд, refresh 180 секунд; неполные/неудачные проверки повторяются через 30 секунд.
|
||||
Во время штатного refresh доступен прежний результат до TTL; его возраст не обновляется.
|
||||
Использованные тестовые порты не переиспользуются в течение жизни ядра, включая reload STUN.
|
||||
История портов освобождается только при окончательном destroy ядра.
|
||||
`stun_nat_recheck(instance)` явно повторяет измерение на новых портах, в том числе после смены маршрута без смены IP.
|
||||
Публичные операции выполняются в uasync-потоке экземпляра.
|
||||
|
||||
INFO содержит начало измерения и итог с адресами, классификацией и причинами UNKNOWN.
|
||||
DEBUG содержит этапы, transaction ID, повторы, RTT, DNS и очистку; WARN/ERROR описывают аномалии.
|
||||
|
||||
Linux-тест `test_stun_nat` использует реальные локальные UDP-сокеты и ускоренные часы:
|
||||
обычный STUN, все классы RFC 5780, недоступные альтернативы, потеря запроса, неверные/поздние ответы,
|
||||
резервные серверы, DNS, нестабильный mapping, отмена и повторная проверка, изменение состава сокетов.
|
||||
Сценарии проверяют сохранность рабочего fd/порта, предварительный EIM?, приоритет ETCP,
|
||||
снятие оценки после TTL, совпадение IP/netif и освобождение таймеров.
|
||||
|
||||
Ссылки: [RFC 5780](https://www.rfc-editor.org/rfc/rfc5780), [STUN RFC 8489](https://www.rfc-editor.org/rfc/rfc8489).
|
||||
+361
-111
@@ -2,136 +2,386 @@
|
||||
|
||||
Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ.
|
||||
|
||||
## Текущая задача
|
||||
[ ] **DM: прямой p2p чат с любым пользователем группы** — отдельная DM-подсистема.
|
||||
ТЗ и архитектура: `/doc/dm_arch.md`. Статус: реализовано (dm_core/dm_mailbox/dm_crypto),
|
||||
тесты `test_dm` (крипто) и `test_dm_e2e` (интеграция). Остались задачи ниже.
|
||||
## Текущие задачи
|
||||
|
||||
[ ] **Звонок (P2P аудио)** — голос 1:1, из CHAT-группы или p2p-чата (DM). Отдельный
|
||||
каталог `src/call/`. Сигналинг + conn_mgr + аудиопоток (Opus) с backpressure
|
||||
(локальный буфер отправки при переполнении роутера) + адаптивный джиттер-буфер
|
||||
(SoundTouch, ускорение до 1.6x) + тоны (глитч/завершение) + watchdog 20с.
|
||||
Android: переключение источника (трубка/громкая/гарнитура).
|
||||
Архитектура: `/doc/chat_call_arch.md`. Статус: спроектировано, ждёт реализации.
|
||||
[+] **Android: приём рации через SCO после отпускания PTT** — исправлено 22.10.2026:
|
||||
Общий MediaAudioController сохраняет разговорный тракт для SCO-only выхода, даже без подписчиков микрофона.
|
||||
AudioTrack рации снова использует USAGE_VOICE_COMMUNICATION: Android учитывает playback как разговорную активность.
|
||||
Добавлены логи фактического routedDevice/mode/SCO и аппаратная проверка после закрытия последнего input.
|
||||
Samsung + C1 PRO: PHONE → Bluetooth → PHONE, 35с приёма без микрофона и холодный запуск SCO без PTT прошли.
|
||||
На Q8 прошли общий захват/переключения/сохранение playback. JVM 145/145, сборки ядра и APK успешны.
|
||||
|
||||
## Рефакторинг: per-instance chat/dm (однопоточный test_dm_e2e)
|
||||
[ ] **Android: просмотр трансляции через SCO без работающей рации** — воспроизведено 22.10.2026:
|
||||
Media-выход с USAGE_MEDIA после закрытия микрофона теряет SCO: mode=0, sco=false,
|
||||
хотя общий контроллер продолжает владеть выходом и сам больше не закрывает разговорный тракт.
|
||||
AudioService учитывает VOICE_COMMUNICATION playback/record для активности MODE_IN_COMMUNICATION.
|
||||
Повтор: adb -s RZ8R72MG64B shell am instrument -w -e output media com.utun.chat/com.utun.chat.data.MediaAudioProbe.
|
||||
Радио с USAGE_VOICE_COMMUNICATION проходит ту же проверку, media-вариант её не проходит.
|
||||
Сохранить media-назначение/громкость обычных выходов; для SCO переключать аппаратный AudioTrack на VOICE_COMMUNICATION.
|
||||
При пересоздании только аудиовыхода сохранить недописанный PCM, учесть уже записанный буфер и непрерывный played;
|
||||
видео и его кодеки не перезапускать. Проверить оба направления смены выхода и длительный playback без микрофона.
|
||||
|
||||
Сделано (глобальные синглтоны переведены на `UTUN_INSTANCE`):
|
||||
- `chat_setting` → `inst->chat_settings` (`struct chat_setting_state`, state-level API для парсера конфига + instance-level для runtime).
|
||||
- `chat_event` → `inst->chat_event_handler` (обработчик теперь получает `inst`).
|
||||
- `chat_core` (`g_cc`) → `inst->chat_core` (`struct chat_core_ctx*`).
|
||||
- `chat_sync` (`g_cs`) → `inst->chat_sync`.
|
||||
- `member_sync` callback-списки (`g_props_cbks`/`g_apply_cbks`) → в `chat_core_ctx`.
|
||||
- `chat_join` (`g_keys`) → в `chat_core_ctx`.
|
||||
- `chat_msg` media-счётчики/backfill → в `chat_core_ctx`.
|
||||
- `dm_core` (`g_dm`) → `inst->dm`; `dm_mailbox` (`g_mb`) → `inst->dm_mailbox`.
|
||||
- `chat_headless_control` (`g_hc`) → `inst->headless`.
|
||||
- `chat_whisper` — сигнатуры `init/available/trigger` принимают `inst`.
|
||||
- API протащено через `inst` (chat_core_*/dm_*), обновлены call-сайты в src/ (headless, topo, media_delivery, auto_socket, config_parser).
|
||||
- `test_dm_e2e.c` переписан на однопоточный: один `uasync`, три `utun_instance` (A,B,C), master state-machine, без fork. Фаза 2 (B offline) — `utun_instance_destroy(B1)` + пересоздание B2.
|
||||
- Попутно исправлен 1-байтовый overflow в `dm_mailbox.c: mb_route_send` (`u_malloc(1+body_len)` → `1+1+body_len`).
|
||||
[+] **media_async: файловые примитивы и точные контракты .h** — исправлено 22.10.2026:
|
||||
ma_copy_file проверяет существование/идентичность, сохраняет один regular-файл при ./ или hardlink;
|
||||
ссылки назначения отклоняются, копирование идёт через соседний temp с проверенными записью/close/publish.
|
||||
Общие примитивы lib/file_io используются также video, сохраняя публикацию видео только в completion.
|
||||
ma_uuid возвращает ошибку RNG и обнуляет выход; media_index не записывает результат при отказе media/block UUID.
|
||||
ma_file_size и переменные вызывающего кода — int64_t; проверяются смещения, число блоков и размеры выделений.
|
||||
Обход передаёт ошибки, пропускает ссылки/reparse points, проверяет длину/глубину; Windows использует wide API.
|
||||
Контракты .h и service_lifecycle описывают синхронные отказы, лимит 16 до completion и отсутствие отката work при отмене.
|
||||
Неверные аргументы дают -1, closing/destroy — CANCELLED; повторный вход в destroy диагностируется и отклоняется.
|
||||
Новый test_media_primitives проверяет сохранность при I/O/fdopen ошибках, RNG/seek без записей в БД,
|
||||
sparse-размеры на границах 2/4 ГиБ, ошибки обхода, ссылки/глубину и ownership задач с внедрением ошибок.
|
||||
Все шесть тестов media_primitives/video/media_index/attachment_send/services/dm_worker прошли ASAN/UBSan с detect_leaks=1.
|
||||
Общий check.sh: 176/176 и proxy/burst/load. Windows runtime/сборка не проверялись: кросс-компилятора в окружении нет.
|
||||
Логи: /tmp/media-primitives-full-check-final.log, /tmp/media-primitives-final.log, /tmp/media-asan-test_*.log.
|
||||
|
||||
Статус сборки: `make -C src` (libutun.a + бинарник utun) и `make -C tests` — OK.
|
||||
[ ] **media_index_commit: не обрезать длинный location** — статическая проверка 22.10.2026:
|
||||
Относительный dest_path записывается через snprintf в location[512] без проверки длины;
|
||||
register_async допускает dest_path до 1023 байт, а media_base может не сокращать его до 511 байт.
|
||||
При таком пути в БД попадёт обрезанное имя. Проверить сценарий на реальном вложенном пути,
|
||||
затем отклонять непредставимый location до INSERT либо согласовать размеры со всеми читателями.
|
||||
Найдено при сверке вызывающего кода media_async; воспроизведение и исправление отложены.
|
||||
|
||||
### Осталось (по порядку)
|
||||
[+] **test_stream_service: убрать зависимость bootstrap-проверки от гонки TCP-handshake** — исправлено 22.10.2026:
|
||||
Падение на строке 409: REINIT=0 при probes=1/subflows=1/failures=0 и доставленной первой пачке.
|
||||
Встречный TCP-линк успевает сообщить peer_reset_id до ETCP CONFIRM: начальная эпоха уже известна,
|
||||
поэтому etcp_session_receive не вызывает REINIT. При другом порядке событий REINIT=1 и тест проходит.
|
||||
Холодная подписка проверяет одну пробу/выдачу и доставку без ошибок, число начальных REINIT диагностическое.
|
||||
Отдельная автоматическая подписка удерживает INFO_RESP, выполняет настоящий REINIT и продолжает ту же пробу.
|
||||
Проверяются новые epoch/nonce, отсутствие лишних проб/CLOSED и пустой локальный буфер перед получением DATA.
|
||||
Ручные проверки старых ответов и исходного deadline сохранены; ожидания привязаны к поколениям и событиям.
|
||||
Добавлен --bootstrap-only: 8 повторов с ускоренными и 2 с обычными часами прошли.
|
||||
Полный test_stream_service прошёл ASAN/UBSan с detect_leaks=1; check.sh — 175/175 и proxy/burst/load.
|
||||
Изолированная потеря pending-пробы вызвала ожидаемый отказ новой автоматической проверки; мутация убрана.
|
||||
Логи: /tmp/stream-service-original-failure.log, /tmp/stream-bootstrap-repeat-*.log,
|
||||
/tmp/stream-service-asan-final.log, /tmp/stream-bootstrap-mutant.log, /tmp/stream-bootstrap-full-check-final.log.
|
||||
|
||||
[+] **test_dm_e2e: зависание в utun_instance_destroy(B1)** (фаза P_B_OFFLINE) — сделано.
|
||||
Причин было несколько (не одна):
|
||||
1. `lib/u_async.c`: если ближайший таймер уже истёк на входе в `uasync_poll`,
|
||||
`get_next_timeout` возвращал {0,0}, `timeout_ms=-1` → `epoll_wait(-1)` блокировался
|
||||
навсегда, а `process_timeouts` (вызывается только после epoll_wait) не запускался.
|
||||
Фикс: `if (timeout_ms == -1 && heap не пуст) timeout_ms = 0`.
|
||||
2. `routing_layer/etcp_router.c`: `router_send_conn` с group_id=0 (глобальная
|
||||
маршрутизация DM/mailbox) не находил маршрут (`topo_groups_find(0)==NULL`).
|
||||
Фикс: fallback на `instance_find_conn(inst, remote)` при group==NULL.
|
||||
3. `dm/dm_core.c`: `dm_on_conn_status` PULL-ил только беседы с подключившимся пиром;
|
||||
B2 подключается к storage (C), а беседа — с A. Фикс: на подъём любого соединения
|
||||
PULL для всех бесед, чей пир не подключён напрямую.
|
||||
4. `dm/dm_mailbox.c`: не было ACK после PULL_RESP → storage не чистил dm_mail.
|
||||
Фикс: PULL_RESP теперь несёт sender, получатель шлёт MB_SUBCMD_ACK.
|
||||
5. `utun_instance.c`: `dm_core_init` ставил deliver-cb до `dm_mailbox_init` →
|
||||
mailbox не находился, deliver-cb не регистрировался. Фикс: mailbox init раньше core.
|
||||
`test_dm_e2e` проходит (несколько прогонов), `test_dm` тоже.
|
||||
[+] **video: сохранность файлов, JPEG и подготовка MP4** — исправлено 22.10.2026:
|
||||
Одинаковые файлы/алиасы и недопустимые пути отклоняются без изменения исходника.
|
||||
JPEG и MP4 пишутся в эксклюзивный временный файл; запись, drain, trailer и close проверяются.
|
||||
Только успешный completion публикует результат; отмена до callback сохраняет старое назначение.
|
||||
Превью получает задержанные B-кадры. MOV/H.264/AAC проходит remux в MP4; остальные профили
|
||||
перекодируются с audio FIFO, сохранением временного смещения и хвоста ресемплера.
|
||||
Атомарный global slot удерживается до completion, ошибки имеют точную фазу и контекст в логе.
|
||||
Контракты описаны в src/video/video.h и doc/service_lifecycle.md.
|
||||
test_video проверяет реальные кадры/сигнал, 1080p→720p, PCM/MP3/AAC, resample 12345→12000 Гц,
|
||||
файловые ошибки, отмену, повтор из callback и конкурентные start; test_video_no_ffmpeg — стабы.
|
||||
Оба теста, test_services и test_attachment_send прошли ASAN/UBSan с detect_leaks=1.
|
||||
Отдельная конфигурация всего ядра/тестов без FFmpeg собрана, оба video-теста прошли.
|
||||
Логи: /tmp/video-asan-final.log, /tmp/video-services-asan.log, /tmp/video-attachment-send-asan.log,
|
||||
/tmp/video-without-ffmpeg-build-final.log; общий прогон: /tmp/video-full-check-final.log.
|
||||
|
||||
[+] **chatgui-android: переезд на per-instance API** — сделано.
|
||||
`instance_lite.c/h` (+`instance_lite_get_instance()`), `jni_bridge.c`, `standby.c`,
|
||||
`headless_control.c`, `voice_recorder.c`, `attachment_sender.c`, `video_sender.c`,
|
||||
`photo_sender.c` — все call-сайты переведены на `inst`. `libutun_lite` компилируется,
|
||||
`jni_bridge.c` — синтакс-чисто. (headless-линковка сломана ПРЕДЫДУЩЕ — instance_lite.c
|
||||
ссылается на jni_bridge-функции, не включённые в headless-сборку; отдельная задача.)
|
||||
[+] **tcp_io: владение connect-таймером и повторный FIN** — 21.10.2026:
|
||||
Замена таймаута сохраняет старый таймер при ошибке создания нового; <=0 отменяет таймер.
|
||||
Возвращаемые ошибки и диапазон INT_MAX/10 проверяются API и TCP-прокси; connect/error/destroy отменяют таймер.
|
||||
FIN различает fin_pending и fin_local, ошибка shutdown идёт в on_error без ложного on_fin_sent.
|
||||
Уточнены отложенное освобождение, синхронные callbacks очередей и платформенный выбор IPv6/маршрута.
|
||||
Проверено: 15 сценариев test_tcp_io, test_tcp_io_flush, test_proxy_regressions и test_socks_http_proxy
|
||||
(16 загрузок с побайтной сверкой). test_tcp_io и flush также прошли ASAN с detect_leaks=1 без ошибок.
|
||||
Логи: /tmp/tcp-io-fix-unit.log, /tmp/tcp-io-fix-asan-unit.log, /tmp/tcp-io-fix-asan-flush.log,
|
||||
/tmp/tcp-io-fix-proxy-regressions.log, /tmp/tcp-io-fix-socks-http.log.
|
||||
|
||||
[+] **test_chat_join: segfault** (регресс рефакторинга) — сделано.
|
||||
`chat_join` хранил `join_keys` в `chat_core_ctx` (CC(inst)), а тест использует chat_join
|
||||
без chat_core → NULL-deref. Фикс: `join_keys` вынесен в `UTUN_INSTANCE.join_keys`
|
||||
(у chat_join собственный init/destroy).
|
||||
[ ] **Desktop publisher: проверить восстановление после недопустимого FPS в Xvfb-тесте** — 20.10.2026:
|
||||
Широкий screenAndWindowCapture прошёл закрытие управления с active=1 и продолжением публикации,
|
||||
затем не получил видео revision>=3 после запроса fps=61 и возврата engine.config().video.fps к 15.
|
||||
Перед этим применены VP9/rotation=90; worker продолжает кодировать generation=4.
|
||||
Причина и связь с UI-изменениями не установлены; отдельная проверка закрытия управления PASS.
|
||||
Лог /tmp/desktop-stream-controls-xvfb.log, тест tools/chatgui/tests/test_stream_publish.cpp.
|
||||
|
||||
[ ] **BGP-гонка: дропнутый REQUEST_TABLE** (отдельная задача, как договорились).
|
||||
[ ] **Проверить одновременную собственную и удалённую трансляции в multiview** — 20.10.2026:
|
||||
Собственный preview и удалённые readers по отдельности проверены; их совместный живой сценарий в одном канале не проверялся.
|
||||
На desktop/Android проверить переход в управление своей публикацией и возврат, fullscreen/Back/Esc,
|
||||
сохранение удалённых readers и общего звука, отсутствие местного звука и остановку собственной публикации.
|
||||
|
||||
**Место:** `topo_group_new_conn()` в `src/routing_layer/topo_group.c:571`. Дедуп-ветка
|
||||
«conn already in senders_list» (строки 579–587) делает `return` БЕЗ повторной отправки
|
||||
`topo_group_send_table_request()`.
|
||||
[ ] **Multiview desktop: terminal CLOSED оставляет аудиовход и timer** — проверка 20.10.2026:
|
||||
fail(reason, false) ставит complete=true/failed=false, но tick снимает audio только по ended/failed.
|
||||
После STREAM_CLOSE_GROUP воспроизведено complete=1, failed=0, attached=1, timer=1.
|
||||
При valid=0 каталог сохраняет плитки, поэтому состояние живёт до ручного закрытия окна.
|
||||
Отделить завершение от разрешения retry и освобождать output при любом terminal CLOSED.
|
||||
Временный regression-проверочный стенд и лог: /tmp/multiview-review/test_stream_view.cpp,
|
||||
/tmp/multiview-review/lifecycle.log. Исходники реализации при проверке не менялись.
|
||||
|
||||
**Зачем дедуп:** тот же `ETCP_CONN` стреляет `ETCP_CONN_STATUS_UP` дважды (UDP-линк,
|
||||
затем TCP-линк). Повторная обработка задваивает `active_conn_count`, из-за чего
|
||||
переподключение не стартует. Поэтому `return` в дедупе — правильный.
|
||||
[ ] **Проверить ожидание запросов ключей в test_encrypted_attachment** — 21.10.2026:
|
||||
До rebase исправлений control_server: check.sh вне sandbox 165/166; отдельный повтор также
|
||||
phase=5/heartbeats=12001, ожидание двух pending incoming-запросов после перезапуска автора.
|
||||
Логи: /tmp/control-server-full-check.log, /tmp/control-server-attachment-recheck.log;
|
||||
артефакты общего прогона: /tmp/utun_encrypted_attachment_6JMO0c. Код вложений в этой работе не менялся.
|
||||
|
||||
**В чём гонка:** REQUEST_TABLE — это то, что заставляет пира отдать свою таблицу
|
||||
(NODEINFO/узлы группы). Сценарий «разнесённого старта»:
|
||||
1. A↔C соединение поднято. A шлёт C `REQUEST_TABLE` для группы G (`topo_group_new_conn`
|
||||
→ `topo_group_send_table_request`, `topo_group.c:602`).
|
||||
2. У C группа G ещё **не создана** (создаётся позже, по мере загрузки каналов/синка).
|
||||
C получает `REQUEST_TABLE`, но `topo_group_handle_request_table` не находит G →
|
||||
дропает запрос.
|
||||
3. C наконец создаёт G (`topo_groups_create_group` → обход connections →
|
||||
`topo_group_new_conn(G, conn_A)` на стороне C) → C шлёт A свой `REQUEST_TABLE`.
|
||||
A отвечает своей таблицей → **C узнаёт A**.
|
||||
4. Но A больше не шлёт C `REQUEST_TABLE` → **A так и не узнаёт C** в группе G.
|
||||
Итог: при разнесённом старте A не видит C в CHAT-группе (односторонняя видимость).
|
||||
[ ] **Разобрать нестабильную загрузку storage-узлов в test_media_delivery_chat** — 19.10.2026:
|
||||
В том же общем прогоне storage st1/st2 не собрали файл, n2 исчерпал попытки блока 0 (15/19 внутренних проверок).
|
||||
Отдельный повтор прошёл 19/19. Причина не установлена; сравнить маршруты и готовность storage под общей нагрузкой.
|
||||
Логи: tests/logs/test_media_delivery_chat.log, /tmp/phone-busy-media-recheck.log.
|
||||
Повторилось 21.10 при аудите ID сервисов: 164/165, те же 15/19 внутренних проверок.
|
||||
Отдельный запуск и 16 параллельных повторов прошли. Причина остаётся открытой;
|
||||
тест теперь сохраняет подробный media/router лог и БД при отказе в /tmp/utun_mdc_*/.
|
||||
Логи /tmp/service-ids-full-check.log, /tmp/service-ids-media-chat-debug.log, /tmp/service-ids-media-chat-stress/.
|
||||
|
||||
**Фикс (предполагаемый):** в дедуп-ветке не просто `return`, а предварительно
|
||||
идемпотентно `topo_group_send_table_request(group, conn)`:
|
||||
```c
|
||||
if (((struct TOPO_GROUP_CONN_ITEM*)se->data)->conn == conn) {
|
||||
topo_group_send_table_request(group, conn); /* повторно, идемпотентно */
|
||||
DEBUG_INFO(..., "conn already in senders_list, re-request table (%s)", conn->log_name);
|
||||
return;
|
||||
}
|
||||
```
|
||||
`topo_group_send_table_request` не ведёт состояния — просто шлёт REQUEST_TABLE,
|
||||
пир отвечает снимком таблицы (идемпотентно). Лишний дубль таблицы безвреден.
|
||||
[ ] **Разобрать нестабильное ожидание ACK в test_local_transport** — 19.10.2026:
|
||||
Общий check.sh: 161/162, chat_adapter упал на строке 173 при ожидании LOCAL_DELIVERY_FORBIDDEN для request=508.
|
||||
В логе получатель дважды отклонил приглашение и отправил ACK; отправитель до проверки получил только late ACK request=507.
|
||||
Отдельный повтор прошёл. Причина не установлена; проверить доставку UDP и шаги ускоренных часов под параллельной нагрузкой.
|
||||
Логи: /tmp/config-name-local-transport-failure.log, /tmp/config-name-local-transport-retry.log.
|
||||
Ещё один общий прогон 19.10: 161/162, chat_adapter упал на строке 145 при ожидании доставки request=501.
|
||||
Получатель принял приглашение и дважды отправил ACK; до проверки в логе отправителя нет приёма этого ACK. Отдельный повтор прошёл.
|
||||
Логи: /tmp/utun-stream-preview-local-transport-failure.log, /tmp/utun-stream-preview-local-transport-retry.log.
|
||||
|
||||
**Замечание:** в однопоточном `test_dm_e2e` (и `test_chat_join_e2e`) из-за
|
||||
детерминированного порядка группа успевает создаться до прихода REQUEST_TABLE —
|
||||
поэтому тест проходит. Реальная гонка остаётся для асинхронных сетей. Отдельный
|
||||
воспроизводимый тест для этой гонки пока не написан.
|
||||
[ ] **Разобрать нестабильную проверку аудиопауз desktop-трансляции** — 17–20.10.2026:
|
||||
test_stream_view::realH264RenderingAndClose нестабильно получает silenceGaps=3–4 вместо 2.
|
||||
Зафиксированы pcm_short с got=96/480 и 288/480; видео продолжало выводиться.
|
||||
Сбой повторялся с общей нагрузкой и после её завершения; изолированные повторы проходили.
|
||||
Проверить актуальные PCM workers/часы под нагрузкой; единичный PASS не подтверждает устранение.
|
||||
Логи: /tmp/stream-zoom-full-view-audio-gap.log, /tmp/stream-zoom-full-view.log,
|
||||
/tmp/multiview-review/desktop-review.log, /tmp/multiview-review/audio-original.log.
|
||||
|
||||
[ ] **«packet undecryptable» на линках C** (вторичное). A, узнав B через BGP, через
|
||||
`topo_group_connect` пытается поднять прямой A↔B линк (`seskey 4777737b`), но B не
|
||||
настроен принимать A → crypto-шум каждые ~1с. Разобраться с авто-подключением
|
||||
(не пытаться соединяться с узлами, для которых нет `[client]`/ncd-конфигурации,
|
||||
либо обрабатывать неудачу без спама).
|
||||
[ ] **Устранить коллизию порта при перезапуске узла в test_dm_e2e** — 16.10.2026:
|
||||
Общий check.sh при проверке Windows-курсора: 155/156, test_dm_e2e упал в phase=9 на создании B2.
|
||||
Повторный bind 127.0.0.1:33843 вернул Address already in use; отдельный повтор теста проходит.
|
||||
Проверить владение/резервирование порта между остановкой B и созданием B2 при параллельном прогоне.
|
||||
Логи: /tmp/win-stream-cursor-dm-e2e-failure.log, /tmp/win-stream-cursor-dm-e2e-retry.log.
|
||||
|
||||
[+] **Обновить GUI call-сайты `tools/chatgui` (Qt, desktop)** — сделано.
|
||||
- В `chat_core.h` вынесены trampoline-структуры из `.c`: `update_my_name_arg`,
|
||||
`save_ui_state_arg`, `chat_setting_arg` (были в chat_profile.c/chat_core.c).
|
||||
- `gui_bridge.h/impl`: `gui_bridge_set_inst()`/`gui_bridge_get_inst()` (+`g_inst`).
|
||||
- `utun_node.cpp`: `gui_bridge_set_inst(m_instance)`, `chat_event_set_handler(m_instance,…)`,
|
||||
`chat_core_sync_my_addresses(m_instance)`.
|
||||
- Обновлены ~45 call-сайтов в 16 файлах (`mainwindow`, `messagelist`, `messagedelegate`,
|
||||
`memberlistmodel`, `memberpropsdialog`, `accountlist`, `invitedialog`, `inviteby`,
|
||||
`joindialog`, `settingsdialog`, `channelsettingsdialog`, `soundsettingspage`,
|
||||
`audiodevicesettingspage`, `storagesettingspage`, `statuspage`, `connmonitorwindow`).
|
||||
- Сборка `cmake --build . --target vibechat` — OK.
|
||||
Попутно: `chat_sync_connect_from_invite` писал `cs->pending_*` на вызывающем (GUI) потоке —
|
||||
перенёс в `cm_invite_trampoline` (uasync-поток), убрав гонку; заодно исправил утечку
|
||||
`inv->addrs_data` и добавил null-check `cs` в трамплине.
|
||||
[ ] **Проверить Windows-трансляцию на исходной машине после исправлений playback/stop** — 16.10.2026:
|
||||
Цикл ожидания позднего аудио исправлен; join и закрытие микрофона выполняются асинхронно,
|
||||
QVideoFrame::toImage() вынесен в долгоживущий image worker. Linux/Xvfb-регрессии проходят.
|
||||
Осталась свежая трансляция Windows → Samsung и stop после пересборки Windows: исходный Qt backend ещё не проверен.
|
||||
На RYS HFR USB2.0 Camera проверить сообщение об отсутствии PCM, отзывчивость при завершении WASAPI,
|
||||
отказ/согласие на CPU после отказа NVENC. Причина frames=0 микрофона пока не установлена;
|
||||
подробный AudioDiag реализован только для Linux. libx265 3440×1440 на исходном CPU выдаёт около 2–3fps.
|
||||
Логи: 4.log, /tmp/stream-viewer-new-run.log, /tmp/late-audio-test.log,
|
||||
/tmp/stream-persistent-image-xvfb.log, /tmp/stream-audio-stop-full-check.log.
|
||||
|
||||
[+] **Прогон всех тестов** — сделано. `make clean && make -j4` OK; все тесты проходят
|
||||
(кроме `test_auto_socket_dynamic` — пропуск «requires root»). `test_dm_e2e` стабилен
|
||||
в нескольких прогонах. В `test_dm_e2e` при teardown остаётся timer-leak (~68 узлов,
|
||||
`router_no_route`), не влияет на результат — стоит разобрать отдельно.
|
||||
[ ] **Проверить появление значка свежей трансляции на SM после сна через colo2** — 16.10.2026:
|
||||
Пробуждение и перезапуск SM во время сна уже проверены на обновлённом colo2:
|
||||
новый линк снимает прежний таймер, Merkle завершается без ожидания полного сна 120с.
|
||||
Осталась визуальная проверка значка новой desktop-трансляции после возврата SM в foreground;
|
||||
в прежнем прогоне активных источников не было (count=0).
|
||||
Логи: /tmp/stream-wake-live-{sm,colo2}-*.log.
|
||||
|
||||
[ ] **Проверить остаточные паузы видеозвонка на актуальном playout** — 15.10.2026:
|
||||
После исправления TX flush живой звонок 12694689407894286842 дал SM 29.87fps без drops,
|
||||
desktop 29.91fps/3 late drops за последние 20с; отдельные промежутки SM доходили до 63.8мс.
|
||||
В старой трассе краткие invalid audio clock сбрасывали якорь, точная причина invalid не была видна.
|
||||
После переработки playout/ACK и чтения аудиочасов 19–20.10 повторить оба направления на устройствах,
|
||||
сопоставить gap_pts_us/gap_rx_us/gap_dispatch_us и причины недоступности часов, если паузы сохраняются.
|
||||
Текущий контракт и результаты поздних проверок: doc/call_video.txt.
|
||||
|
||||
[ ] **Доступность разговорного динамика в UI звонка** — при подключённой проводной гарнитуре Samsung убирает
|
||||
EARPIECE из Telecom endpoints, но кнопка остаётся доступной по наличию физического встроенного устройства.
|
||||
Нажатие создаёт отложенный запрос, который исполняется после отключения гарнитуры. Согласовать UI с доступными endpoints.
|
||||
Также выбор проводной гарнитуры в CallScreen сейчас запрашивает BLUETOOTH_CONNECT при отсутствии этого разрешения;
|
||||
проверку разрешения нужно привязать к выбранному типу гарнитуры.
|
||||
|
||||
[ ] **Ручная приёмка видеозвонков Android и ChatGUI после последних исправлений** — 14–20.10.2026:
|
||||
Сигналинг, CAPS, реальные codecs/камера/render и A/V подключены; живые звонки уже проводились.
|
||||
Осталась полная проверка после изменений Full HD/50–60fps, HEVC High tier, Surface ACK и фоновых codec probes:
|
||||
Windows ↔ Samsung/Q8, камеры и оба направления, первый и повторный звонок, смена камеры/разрешения/кодека/битрейта,
|
||||
A/V, завершение и освобождение Surface/видеопорта. Аппаратные probes не заменяют эту проверку.
|
||||
Архитектура, контракты и выполненные проверки: doc/call_video.txt, doc/chat_call_arch.md.
|
||||
|
||||
[ ] **Тесты: нестабильное завершение test_stcp_traffic** — 14.10.2026:
|
||||
В полном прогоне после NCD CLOSE server получил 1029466/1048576 bytes (done=0), client — 1048576 (done=1).
|
||||
Упала проверка test_stcp_traffic.c:216; отдельный повтор прошёл. Причина не установлена, сетевой код в задаче zoom не менялся.
|
||||
Проверить завершение двусторонней передачи относительно закрытия handle; первичный лог сохранён: /tmp/stream-zoom-stcp-first.log.
|
||||
|
||||
[ ] **Тесты: диагностировать нестабильную проверку TX snapshot в test_android_stream** — 14.10.2026:
|
||||
В полном прогоне один раз упала строка 177: snapshot успешен и 0 < video_bytes < 500000 после test_clock_run_for(ua, 1000).
|
||||
Сам publisher обновляет snapshot таймером 1000; текущий CHECK не выводит result/sample_us/video_bytes.
|
||||
Отдельный повтор и следующий полный прогон прошли. Добавить значения и проверить ожидание конкретного снимка;
|
||||
причина не установлена, Kotlin-аудиовывод этот C-тест не вызывает.
|
||||
Логи прогонов: /tmp/stream-audio-check-host.log, /tmp/stream-audio-native-retry.log, /tmp/stream-audio-check-final.log.
|
||||
|
||||
[ ] **Общий AGC: клиппинг после резкого роста сигнала при max_gain=45dB** —
|
||||
на SM при проверке CAMCORDER с настройками рации rise=6/release=30 наблюдались
|
||||
4581 обрезанный отсчёт за интервал около 1s. Это общий алгоритм radio/stream,
|
||||
плавное снижение gain может не успеть за скачком входа. Проверить ограничение пиков
|
||||
без щелчков и ухудшения адаптации. Лог: `/tmp/stream-camcorder-sm-live.log`.
|
||||
|
||||
[ ] **Desktop: выход после SIGTERM оставляет GUI после остановки ядра** — 13.10.2026:
|
||||
при обновлении live-плеера старый vibechat завершил ядро/worker, но процесс остался
|
||||
в QCoreApplication::exec с работающим audio output. В логах также DM SELECT после
|
||||
уничтожения SQLite (`bad parameter or other API misuse`). Проверить отмену Quit
|
||||
в MainWindow::closeEvent (всегда ignore), порядок уведомлений и отключение GUI от БД.
|
||||
Снимок: `/tmp/stream-desktop-shutdown-backtrace.log`, лог vibechat 13:04:09.
|
||||
|
||||
[ ] **Стриминг SM → colo2 → desktop: восстановление частично остановившихся субпотоков** —
|
||||
13.10.2026: помощь подключилась за ~8 с, все 8 субпотоков через colo2 приняты с общего индекса 5844.
|
||||
Позже шесть субпотоков перестали продвигаться около seq=111625; четыре restart не восстановили
|
||||
просмотр, desktop завершился по таймауту в 11:10:48 (colo2: 12:10:48). На SM публикация продолжилась,
|
||||
colo2 снял свою подписку через 3 с. В том же интервале сервер видит RX timeout/переподключения TCP к SM;
|
||||
первопричина остановки выдач пока не установлена: stream на colo2 наследовал error, детальных логов не было.
|
||||
Повторить с заранее включённой диагностикой stream/etcp на SM и colo2, проверить upstream handles,
|
||||
аренды, позиции и повторное согласование. Логи: `/tmp/stream-live-gui-final.log`,
|
||||
`/tmp/stream-live-colo2-final.log`, `/tmp/stream-live-sm-final.log`.
|
||||
Повтор 13.10 со stereo: до 15:20 desktop показывал ~30fps, затем video/audio очереди опустели;
|
||||
decodeFps упал до 7–18 и viewer завершился по таймауту в 15:21:13 после пяти restart.
|
||||
SM при этом сохранял ~30fps без dropped. Новые логи: `/tmp/stream-stereo-desktop-delivery-stall.log`,
|
||||
`/tmp/stream-stereo-colo2-delivery-stall.log`, `/tmp/stream-stereo-sm-delivery-stall.log`.
|
||||
|
||||
[ ] **Android: отмена асинхронного запуска до готовности ядра** — при проверке
|
||||
мониторинга 12.10.2026 обнаружено: `NativeLib.stop()` пропускает native stop,
|
||||
если `isReady()==false`, а `instance_lite_stop()` возвращается при `g_inst==NULL`.
|
||||
Поэтому запрос stop до завершения конструктора может не отменить запускаемый
|
||||
поток. Согласовать отмену с состоянием потока и единым сигналом завершения;
|
||||
проверить stop до SERVICE_STARTED, отказ конструктора и последующий start.
|
||||
|
||||
[ ] **Android: повторные отказы persisted node snapshots** — замечено 12.10.2026
|
||||
на устройстве D2AR10P3600820 при проверке live media. В сохранённой БД ряд узлов
|
||||
регулярно получает `invalid persisted snapshot` в `topo_node_sqlite_snapshot_load`,
|
||||
затем NCD сообщает `node not found`; повторные попытки засоряют лог.
|
||||
Лог: `/tmp/stream-av-device-initial.log`. Причина пока не установлена: текущий
|
||||
текст объединяет отказ deserialize, несовпадение ID и отказ verify. Разделить
|
||||
диагностику этих ветвей и проверить повторные попытки загрузки отвергнутого снимка.
|
||||
Существующую БД устройства при проверке стриминга не очищали.
|
||||
|
||||
[ ] **STUN IPv6** — отдельный этап: IPv6 endpoint/config/DNS и XOR-MAPPED,
|
||||
scope/netif, тесты IPv6/NAT66. IPv4-оценки не переносить на IPv6 или TCP.
|
||||
|
||||
[ ] **check.sh: изоляция сетевых integration-тестов** — после основного набора
|
||||
автоматически запускает check-proxy/burst/load при доступном sudo. Скрипты
|
||||
меняют rp_filter, таблицу 100 и iptables главного namespace; cleanup выставляет
|
||||
rp_filter=1 вместо сохранённого значения. Перенести тесты в отдельный namespace
|
||||
и проверять неизменность родительской сети, как в tests/stream_netns.
|
||||
|
||||
[ ] **Стриминг: оставшиеся длительные и масштабные сетевые проверки** — 10–12.10.2026:
|
||||
Реализация ядра, публикации, приёма и автоматического выбора поставщиков завершена; контракт — doc/stream.txt.
|
||||
Есть 16-процессный namespace-стенд с NAT/reverse и выбором UDP/TCP: tests/stream_netns/README.md.
|
||||
Осталось подтвердить покрытие длительной нагрузки, смены адресов/мультилинка, одновременной перестройки
|
||||
нескольких ретрансляторов, нескольких автоматических источников/групп и mobile standby на устройстве
|
||||
(doc/stream.txt, раздел 16). Сохранённые проверки отдельных этапов не закрывают всю матрицу.
|
||||
|
||||
[ ] **Member files: исчерпание кеша результатов массовых изменений** — 10.10.2026:
|
||||
Воспроизведено test_member_files_browser::recursiveDeletion(server-busy): из 76 объектов удаляются 64,
|
||||
следующая команда получает MF_BUSY, остаются 12. storage.c: STORE_RECEIPTS=64, RECEIPT_TB=1200000 (120 секунд).
|
||||
Лимит общий для успешных COMMIT/MKDIR/RENAME/REMOVE; затрагивает также рекурсивное копирование.
|
||||
Нужен отдельный этап управления временем жизни receipts с сохранением идемпотентности повторных запросов;
|
||||
простое вытеснение старых записей нарушает гарантию повторного ответа. После исправления изменить регрессию
|
||||
на успешное удаление более 64 объектов и проверить повторные запросы/потерю ответа.
|
||||
|
||||
[ ] **Desktop: провалы вероятности VAD при компрессии** — диагностика 09.10.2026:
|
||||
Воспроизведение пользователя: длительная гласная, затем временная нечувствительность; ближе к концу лога несколько повторов.
|
||||
Новый лог 14:32:14–14:34:38: на 14:34:07–18 и 14:34:20–38 все 556 окон с RMS > -30dBFS имеют p < 0.5;
|
||||
PCM непрерывен, errors=0, RX-gate не мешает, clips=5 не растёт. Сигнал не исчезает, порог остаётся 0.5.
|
||||
--probe-stream: C-обёртка + реальный AGC + две Silero на одинаковом PCM, отличие — один reset состояния второй модели.
|
||||
Синтезированный flite ah растянут до 24.576с: без AGC/клиппинга p первой секунды следующей речи 0.027 против 0.674 после reset.
|
||||
При AGC 1dB (clips=0) — 0.012 против 0.662. Влияние истории модели подтверждено, провал на десятки секунд целиком не воспроизведён.
|
||||
Лог стенда: /tmp/vad-vowel-context.log; запуск: test_silero_vad lib/silero_vad.onnx --probe-stream /tmp/vad-vowel-context48.pcm 993.
|
||||
Вход стенда: mono s16le/48кГц, reset_window — индекс окна 512/16кГц; режим проверяет без AGC, 1dB и 15dB.
|
||||
Автоматические сбросы в рабочем VAD не добавлялись; test_silero_vad PASS.
|
||||
Уточнение: низкая вероятность держится десятки секунд, затем распознавание восстанавливается; краткий клиппинг это не объясняет.
|
||||
Локальный лог 14:00:10–14:00:58: 15 сбросов AEC при пропусках референса, 16 clipped samples в VAD; vad DEBUG не был включён.
|
||||
Добавлены секундные уровни raw/clean/reference и причина bypass/reset AEC; отдельное предупреждение о клиппинге VAD.
|
||||
Секундный отчёт захвата работает также без PCM: возраст кадра, очередь, максимальная задержка и предупреждение о простое.
|
||||
TRACE каждого окна содержит DC, число нулей и gain вместе с вероятностью/уровнем/паузой между окнами; видны сбросы и RX-gate.
|
||||
В локальном build/vibechat.cfg включены vad=trace, aec/radio/call=debug, general=info; после диагностики вернуть обычные уровни.
|
||||
Стенд --probe повторяет одинаковый PCM без reset: AGC 15/30dB после паузы обрезает пики (468/5625 samples на чистой записи).
|
||||
Полного провала всей фразы почти до нуля стенд не воспроизвёл; нужен повтор живой речи с новыми логами для связи с AEC/AGC.
|
||||
Алгоритмы обработки сигнала пока не менялись; исправление причины выбирается по результатам повтора.
|
||||
|
||||
[ ] **Linux headless CMake: актуализировать зависимости общего ядра** — 07.10.2026:
|
||||
чистая сборка tools/chatgui-android/build_headless останавливается на lib/speex_aec.c:
|
||||
speex/speex_echo.h не найден. utun_setup_sources включает этот исходник, но UTUN_INCLUDE_DIRS не содержит
|
||||
lib/speexdsp/include. Проверить полный состав Speex/voice зависимостей отдельного каркаса;
|
||||
основная Autotools C-сборка и Android NDK-сборка проходят.
|
||||
|
||||
[ ] **Завершить проверку реакций и голосов на SM** — 07.10.2026:
|
||||
Исправления подписей корней и их сохранения реализованы, проверены тестами; local и копия SM проверены.
|
||||
Последующие APK уже устанавливались на SM, поэтому прежнее ожидание подключения ADB неактуально.
|
||||
Осталась явная проверка реакции/голоса на устройстве с текущей БД и сборкой.
|
||||
Разовый скрипт и резервная копия подписей: /home/user/utun2-backups/root-certificates-20261007.
|
||||
|
||||
[ ] **rlottie: разбор предупреждений GCC 15 в импортированных исходниках** — 05.10.2026:
|
||||
при сборке `tools/chatgui/rlottie_src` GCC сообщает `-Wuninitialized` в
|
||||
`VArenaAlloc::reset()`: fFirstBlock/fFirstSize/fFirstHeapAllocationSize читаются
|
||||
после явного вызова деструктора. Проверить сохранение значений до разрушения
|
||||
и повторный reset с реальными выделениями. Также проверить `-Wfree-nonheap-object`
|
||||
в `vcow_ptr`: статическая модель default_s сохраняет базовую ссылку.
|
||||
Лог: `/tmp/utun-rlottie-vendor-build.log`. Сборка библиотеки и подключение её
|
||||
экспортированного CMake-пакета к ChatGUI на Linux прошли; Windows ещё не проверен.
|
||||
|
||||
[ ] **ChatGUI Windows: подтверждение обновления runtime компилятора** — 05.10.2026:
|
||||
`cc-probe.log`: cc1 запускается из корня, но в `tools/chatgui/build` завершается
|
||||
с `0xC0000139`; локальные DLL GCC/math/runtime отличаются от UCRT64.
|
||||
Перед обеими точками сборки добавлено обновление существующих копий этих DLL;
|
||||
раскладка FFmpeg использует для них UCRT64. Проверены обновление, повторный вызов,
|
||||
сохранение кодеков и ошибки. Требуется Windows-прогон `check_cc.bat`: cc1 после
|
||||
обновления, повтор команды Ninja и сборка Opus должны завершиться с кодом 0.
|
||||
|
||||
[ ] **ChatGUI Windows: обновление остальных DLL приложения после обновления MSYS2** — 05–06.10.2026:
|
||||
Пропуск раскладки при наличии основного комплекта уже убран: зависимости проверяются на каждой сборке,
|
||||
Qt DLL/плагины и собственный комплект FFmpeg копируются заново; runtime GCC обновляется отдельно.
|
||||
Осталось определить обновление прочих уже существующих транзитивных DLL: рекурсивный проход
|
||||
по-прежнему копирует только отсутствующие файлы. Сохранить приоритет собственной сборки FFmpeg
|
||||
и проверить запуск приложения после обновления MSYS2.
|
||||
|
||||
[ ] **Android: время жизни config provider** — 03.10.2026: `utun_bridge_init()`
|
||||
передаёт адрес локального `utun_config_provider_t` в `utun_config_set_provider()`,
|
||||
который хранит указатель. После возврата из init он недействителен; перед
|
||||
использованием `cfg_get_*` обеспечить постоянное хранение provider.
|
||||
|
||||
[ ] **Общий лимит check.sh после clean** — 01.10.2026: расчёт по времени предыдущего
|
||||
прогона дал 205с вместе с полной сборкой и оборвал проверку после успешных тестов
|
||||
до `test_uasync_socket_race`. Повторный запуск на готовой сборке с большим лимитом
|
||||
прошёл за 206с, включая proxy/burst/load. Разделить бюджет сборки и запуска тестов
|
||||
либо учитывать clean при расчёте.
|
||||
|
||||
[ ] **Повторно проверить паузы desktop-аудио при звонке с SM** — 01.10.2026:
|
||||
В звонках 939a3ea36f3eeac3/b2a423ed2d36ed83 частота callback падала до 4–40/с,
|
||||
входящий PCM продолжал приходить, очередь росла до нескольких секунд без dropped.
|
||||
Диагностика backend, интервалов callback и capture/playback уже добавлена; позднее менялись аудиовывод и его буферы.
|
||||
Нужен повтор на текущей сборке с CALL_STATS и временными метриками, чтобы проверить сохранение исходного сбоя.
|
||||
Сетевую задержку оценивать отдельно: в одном из прежних звонков DIRECT RTT был 2–3мс.
|
||||
|
||||
[ ] **Измерить оставшуюся задержку запуска аудио SM после исправлений** — 01.10.2026:
|
||||
В исходном звонке 2b9ad4e21a54d8ba первый capture приходил через 1140мс от startAudio;
|
||||
router.start=781мс, startRecording → первый кадр=328мс, AudioTrack.play=327мс.
|
||||
Независимый запуск capture/playback, отмена частичного старта и подробные замеры уже реализованы;
|
||||
позднее добавлен выбор AAudio/AudioTrack. Осталось измерить текущую сборку на SM:
|
||||
callbackMs/btProfileMs/applyMs/refreshMs, первый AudioRecord.read и первая запись playback,
|
||||
начальное накопление очереди. Старые длительности не считать характеристиками текущей реализации.
|
||||
|
||||
[ ] **Гонка invite-файла в test_chat_join_e2e** — `wait_file()` проверяет только
|
||||
существование файла, а `wf()` создаёт его до записи. Читатель иногда получает
|
||||
пустую ссылку и ошибку префикса `utun://`. Публиковать готовый файл атомарно.
|
||||
|
||||
[+] **Linux ONNX Runtime: повторная регистрация схем при запуске теста** — проверено 21.10.2026:
|
||||
Причина в системной сборке `1.23.2+dfsg-6ubuntu1`: Runtime вручную регистрирует схемы,
|
||||
затем динамическая `libonnx.so.1` повторяет регистрацию в `OpSchemaRegistry::map()`.
|
||||
Подтверждено GDB (`/tmp/silero-schema-gdb.log`) и исходниками обеих библиотек.
|
||||
Текущая Linux x64 CMake-сборка переключена на [официальный Runtime 1.23.2](https://github.com/microsoft/onnxruntime/releases/tag/v1.23.2).
|
||||
Архив `onnxruntime-linux-x64-1.23.2.tgz` распакован в `.cache/onnxruntime/`;
|
||||
SHA-256: `1fa4dcaef22f6f7d5cd81b28c2800414350c10116f5fdd46a2160082551c5f9b`.
|
||||
Для существующего include `onnxruntime/onnxruntime_c_api.h` создана ссылка
|
||||
`.cache/onnxruntime/include/onnxruntime` → `../onnxruntime-linux-x64-1.23.2/include`.
|
||||
В CMake явно заданы абсолютные пути: `ONNXRUNTIME_INCLUDE_DIR=<repo>/.cache/onnxruntime/include`,
|
||||
`ONNXRUNTIME_LIB=<repo>/.cache/onnxruntime/onnxruntime-linux-x64-1.23.2/lib/libonnxruntime.so`.
|
||||
Выбор сохраняется в `tools/chatgui/build/CMakeCache.txt`; после удаления cache его нужно повторить.
|
||||
`ldd` подтверждает официальный Runtime без отдельной `libonnx`; весь `test_silero_vad` проходит без `Schema error`.
|
||||
Лог проверки: `/tmp/silero-runtime-test.log`, полный вывод — `tools/chatgui/build/Testing/Temporary/LastTest.log`.
|
||||
|
||||
[ ] **debug_parse_config: формат и индексы категорий** — parser ожидает `:`, документация
|
||||
описывает `=`; преобразование битовой маски осталось после перехода enum на индексы.
|
||||
[Разбор](etcp_full_suite_findings.md).
|
||||
|
||||
[ ] **Повторно проверить crypto-шум при автоподключении в сценарии A–C–B**:
|
||||
В старом test_dm_e2e попытка прямого A↔B через topo_group_connect давала packet undecryptable примерно раз в секунду.
|
||||
Наблюдение относилось к прежним настройкам приёма и архитектуре подключений; устранение не подтверждено.
|
||||
Воспроизвести на текущих NCD/ETCP и установить точную причину по логам до изменения политики подключения.
|
||||
Отсутствие [client] само по себе не считать основанием запрета: CHAT использует собственных NCD-владельцев.
|
||||
|
||||
[ ] **lwip/proxy: утечка pcb в FIN_WAIT_2** — `tcp_shutdown(pcb,0,1)` (half-close по FIN от
|
||||
exit) не ставит `TF_RXCLOSED`, поэтому таймаут FIN_WAIT_2 в `tcp_slowtmr` не срабатывает:
|
||||
если браузер никогда не пришлёт FIN, pcb навсегда остаётся в FIN_WAIT_2 (утечка, не crash).
|
||||
Двойной free по TIME_WAIT уже закрыт (detach по локальному закрытию + guard abort).
|
||||
|
||||
## Открытые флаки
|
||||
|
||||
[ ] **test_etcp_transport: таймаут восстановления REALITY-сессии** — 18.10.2026:
|
||||
в общем `./check.sh` сценарий `reality=1 simultaneous=0 restart=0` завершился
|
||||
`Session recovery timed out: phase=1 stream=15 control=6`. Перед таймаутом —
|
||||
`record socket read failed: err=104`, клиент ждал server-handshake.
|
||||
Отдельный повтор того же бинарника прошёл. Причина нестабильности пока не установлена;
|
||||
изменения Android JNI/UI транспортные исходники не затрагивали.
|
||||
|
||||
[ ] **test_etcp_reconnect** — флаки под параллельной нагрузкой `make check -j4`:
|
||||
phase 4 (reconnect после server restart) таймаутит (`sent=480 recv=0`), при одиночном
|
||||
запуске стабильно `TEST PASSED`. Наш код (chat_join/chat_sync) его не трогает.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-present Silero Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+39
-2
@@ -15,6 +15,8 @@ libuasync_a_SOURCES = \
|
||||
socket_compat.h \
|
||||
platform_compat.c \
|
||||
platform_compat.h \
|
||||
file_io.c \
|
||||
file_io.h \
|
||||
getmyip.c \
|
||||
getmyip.h \
|
||||
swm_min.c \
|
||||
@@ -25,6 +27,10 @@ libuasync_a_SOURCES = \
|
||||
radix.h \
|
||||
serialize.c \
|
||||
serialize.h \
|
||||
video_plane.c \
|
||||
video_plane.h \
|
||||
reed_solomon.c \
|
||||
reed_solomon.h \
|
||||
json_flat.c \
|
||||
json_flat.h \
|
||||
tcp_io.c \
|
||||
@@ -40,17 +46,48 @@ libuasync_a_SOURCES = \
|
||||
dns.c \
|
||||
dns.h \
|
||||
async_dns.c \
|
||||
async_dns.h
|
||||
async_dns.h \
|
||||
speex_aec.c \
|
||||
speex_aec.h \
|
||||
speexdsp/mdf.c \
|
||||
speexdsp/fftwrap.c \
|
||||
speexdsp/kiss_fft.c \
|
||||
speexdsp/kiss_fftr.c
|
||||
|
||||
if HAVE_SILERO_VAD
|
||||
libuasync_a_SOURCES += \
|
||||
silero_vad.c \
|
||||
silero_vad.h
|
||||
endif
|
||||
|
||||
libuasync_a_CFLAGS = \
|
||||
-D_ISOC99_SOURCE \
|
||||
-DDEBUG_OUTPUT_STDERR \
|
||||
-DSQLITE_THREADSAFE=1 \
|
||||
-DDNS_RANDOM=RAND_bytes \
|
||||
-DFLOATING_POINT \
|
||||
-DUSE_KISS_FFT \
|
||||
-DEXPORT= \
|
||||
-g \
|
||||
-I$(top_srcdir)/src \
|
||||
-I$(top_srcdir)/lib \
|
||||
-I$(top_srcdir)/lib/libopus/include
|
||||
-I$(top_srcdir)/lib/libopus/include \
|
||||
-I$(top_srcdir)/lib/speexdsp \
|
||||
-I$(top_srcdir)/lib/speexdsp/include
|
||||
|
||||
if HAVE_SILERO_VAD
|
||||
libuasync_a_CFLAGS += -DHAVE_SILERO_VAD
|
||||
endif
|
||||
|
||||
# Вшитая модель Silero VAD (включается из silero_vad.c)
|
||||
EXTRA_DIST = silero_vad_model.inc \
|
||||
COPYING.silero-vad \
|
||||
wintun-LICENSE.txt \
|
||||
wintun-api-LICENSE.txt \
|
||||
speexdsp/COPYING \
|
||||
liblmdb/LICENSE \
|
||||
liblmdb/COPYRIGHT \
|
||||
soundtouch-master/COPYING.TXT
|
||||
|
||||
# Clean build directory
|
||||
clean-local:
|
||||
|
||||
+1
-1
@@ -251,7 +251,7 @@ static void adns_rearm(struct adns_query* q) {
|
||||
}
|
||||
q->cur_fd = -1;
|
||||
if (fd >= 0) {
|
||||
q->socket_id = uasync_add_socket_t(q->ua, (socket_t)fd, adns_read_cb, adns_write_cb, NULL, q);
|
||||
q->socket_id = uasync_add_socket_t(q->ua, (socket_t)fd, adns_read_cb, adns_write_cb, NULL, "dns_query", q);
|
||||
if (q->socket_id) q->cur_fd = fd;
|
||||
else DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: uasync_add_socket_t(%d) failed", fd);
|
||||
}
|
||||
|
||||
+7
-3
@@ -34,23 +34,27 @@ struct adns_result {
|
||||
struct sockaddr_in addrs[ADNS_MAX_ADDRS]; /* AF_INET, sin_port = 0 */
|
||||
};
|
||||
|
||||
/* res заимствован до возврата callback; handle запроса к этому моменту уже освобождён. */
|
||||
typedef void (*adns_done_cb)(const struct adns_result* res, void* arg);
|
||||
|
||||
struct adns_opts {
|
||||
struct sockaddr_in server; /* sin_family != 0 → использовать этот сервер (иначе системные) */
|
||||
int timeout_ms; /* таймаут на сервер, по умолчанию 2000 */
|
||||
int timeout_ms; /* Таймаут на сервер в мс; <=0 → 2000, округляется вверх до целых секунд. */
|
||||
int max_attempts; /* попыток на сервер, по умолчанию 2 */
|
||||
};
|
||||
|
||||
struct adns_query; /* opaque */
|
||||
|
||||
/* Запускает асинхронный A-запрос. Коллбэк вызывается в потоке event loop.
|
||||
Возвращает handle (NULL при немедленной ошибке: невалидные аргументы/нет памяти). */
|
||||
* Возвращает handle, автоматически освобождаемый до callback либо при отмене.
|
||||
* NULL: неверные аргументы, нехватка памяти, отсутствие DNS-серверов, ошибка создания
|
||||
* резолвера, постановки запроса или регистрации отложенного запуска; callback не вызывается. */
|
||||
struct adns_query* adns_resolve(struct UASYNC* ua, const char* name,
|
||||
const struct adns_opts* opts,
|
||||
adns_done_cb cb, void* arg);
|
||||
|
||||
/* Отмена ожидающего запроса: коллбэк НЕ вызывается. Только из потока loop. */
|
||||
/* Отмена ожидающего запроса: освобождает handle, коллбэк НЕ вызывается. Только из потока loop.
|
||||
* После завершения/отмены handle недействителен; NULL допустим. */
|
||||
void adns_cancel(struct adns_query* q);
|
||||
|
||||
/* Системные DNS-серверы (IPv4). Возвращает количество; out[].sin_port = htons(53).
|
||||
|
||||
+183
-222
@@ -3,223 +3,144 @@
|
||||
#include "mem.h"
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define BLOCK_LEVELS_CHUNK 64
|
||||
#define PENDING_CHUNK 32
|
||||
#include <stdint.h>
|
||||
|
||||
/* История и задержанные блоки ограничены окном настройки. Только запись накапливает output. */
|
||||
struct audio_compressor {
|
||||
audio_compressor_config_t cfg;
|
||||
int enabled;
|
||||
|
||||
int block_samples;
|
||||
int lookback_blocks;
|
||||
int lookahead_blocks;
|
||||
float rise_factor_per_block;
|
||||
float max_gain;
|
||||
float gain_smoothed;
|
||||
|
||||
float* block_levels;
|
||||
size_t block_levels_count;
|
||||
size_t block_levels_cap;
|
||||
|
||||
struct pending_block {
|
||||
int16_t* samples;
|
||||
size_t count;
|
||||
size_t level_index;
|
||||
}* pending;
|
||||
size_t pending_count;
|
||||
size_t pending_cap;
|
||||
|
||||
int16_t* accumulator;
|
||||
size_t accum_count;
|
||||
|
||||
int enabled, configured, failed, block_samples, lookback_blocks, lookahead_blocks;
|
||||
float rise_factor_per_block, release_factor_per_block, max_gain, gain_smoothed;
|
||||
float* block_levels;
|
||||
size_t levels_cap, blocks_seen;
|
||||
int16_t* pending;
|
||||
size_t pending_cap, pending_head, pending_count, accum_count;
|
||||
int16_t* output;
|
||||
size_t output_size;
|
||||
size_t output_cap;
|
||||
|
||||
int dbg_counter;
|
||||
size_t output_size, output_cap;
|
||||
uint32_t clip_count;
|
||||
unsigned dbg_counter;
|
||||
};
|
||||
|
||||
/* Начальное усиление в середине диапазона в дБ. */
|
||||
static float initial_gain(float max_gain) { return max_gain > 0.0f ? sqrtf(max_gain) : 1.0f; }
|
||||
|
||||
struct audio_compressor* audio_compressor_create(void) {
|
||||
struct audio_compressor* ac = u_calloc(1, sizeof(*ac));
|
||||
if (!ac) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "audio_compressor_create: OOM");
|
||||
return NULL;
|
||||
}
|
||||
if (!ac) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: state allocation failed"); return NULL; }
|
||||
ac->enabled = 1;
|
||||
ac->gain_smoothed = 1.0f;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "audio_compressor_create: ok");
|
||||
return ac;
|
||||
}
|
||||
|
||||
void audio_compressor_destroy(struct audio_compressor* ac) {
|
||||
if (!ac) return;
|
||||
for (size_t i = 0; i < ac->pending_count; i++)
|
||||
u_free(ac->pending[i].samples);
|
||||
u_free(ac->pending);
|
||||
u_free(ac->block_levels);
|
||||
u_free(ac->accumulator);
|
||||
u_free(ac->output);
|
||||
u_free(ac);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "audio_compressor_destroy");
|
||||
}
|
||||
|
||||
void audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg) {
|
||||
if (!ac || !cfg) return;
|
||||
ac->cfg = *cfg;
|
||||
if (ac->cfg.sample_rate <= 0) ac->cfg.sample_rate = 48000;
|
||||
if (ac->cfg.channels <= 0) ac->cfg.channels = 1;
|
||||
if (ac->cfg.block_duration_ms <= 0) ac->cfg.block_duration_ms = 20;
|
||||
if (ac->cfg.lookback_ms <= 0) ac->cfg.lookback_ms = 200;
|
||||
if (ac->cfg.lookahead_ms <= 0) ac->cfg.lookahead_ms = 100;
|
||||
if (ac->cfg.target_level <= 0.0f) ac->cfg.target_level = 0.25f;
|
||||
|
||||
ac->block_samples = ac->cfg.sample_rate * ac->cfg.block_duration_ms / 1000 * ac->cfg.channels;
|
||||
ac->lookback_blocks = ac->cfg.lookback_ms / ac->cfg.block_duration_ms;
|
||||
ac->lookahead_blocks = ac->cfg.lookahead_ms / ac->cfg.block_duration_ms;
|
||||
ac->rise_factor_per_block = powf(ac->cfg.rise_rate_per_sec, (float)ac->cfg.block_duration_ms / 1000.0f);
|
||||
ac->max_gain = powf(10.0f, ac->cfg.max_gain_db / 20.0f);
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,
|
||||
"audio_compressor_configure: rate=%d ch=%d block=%dms lookback=%dms lookahead=%dms maxGain=%.0fdB riseRate=%.1f/sec target=%.0fdBFS",
|
||||
ac->cfg.sample_rate, ac->cfg.channels, ac->cfg.block_duration_ms,
|
||||
ac->cfg.lookback_ms, ac->cfg.lookahead_ms, ac->cfg.max_gain_db,
|
||||
ac->cfg.rise_rate_per_sec, 20.0f * log10f(ac->cfg.target_level));
|
||||
u_free(ac->pending); u_free(ac->block_levels); u_free(ac->output); u_free(ac);
|
||||
}
|
||||
|
||||
void audio_compressor_reset(struct audio_compressor* ac) {
|
||||
if (!ac) return;
|
||||
for (size_t i = 0; i < ac->pending_count; i++)
|
||||
u_free(ac->pending[i].samples);
|
||||
ac->pending_count = 0;
|
||||
ac->block_levels_count = 0;
|
||||
ac->accum_count = 0;
|
||||
ac->output_size = 0;
|
||||
ac->gain_smoothed = 1.0f;
|
||||
ac->dbg_counter = 0;
|
||||
ac->pending_head = ac->pending_count = ac->accum_count = ac->blocks_seen = ac->output_size = 0;
|
||||
ac->gain_smoothed = initial_gain(ac->max_gain);
|
||||
ac->clip_count = ac->dbg_counter = 0;
|
||||
ac->failed = 0;
|
||||
}
|
||||
|
||||
void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled) {
|
||||
if (!ac) return;
|
||||
ac->enabled = enabled ? 1 : 0;
|
||||
/* Ноль lookahead означает отсутствие задержки; defaults остальных полей задаются явно здесь. */
|
||||
int audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* config) {
|
||||
if (!ac || !config) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: missing configuration"); return -1; }
|
||||
audio_compressor_config_t cfg = *config;
|
||||
if (!cfg.sample_rate) cfg.sample_rate = 48000;
|
||||
if (!cfg.channels) cfg.channels = 1;
|
||||
if (!cfg.block_duration_ms) cfg.block_duration_ms = 20;
|
||||
if (!cfg.lookback_ms) cfg.lookback_ms = 200;
|
||||
if (!cfg.target_level) cfg.target_level = 0.25f;
|
||||
if (!cfg.rise_rate_per_sec) cfg.rise_rate_per_sec = 4.0f;
|
||||
if (!cfg.release_rate_per_sec) cfg.release_rate_per_sec = cfg.rise_rate_per_sec * 5.0f;
|
||||
if (cfg.sample_rate < 8000 || cfg.sample_rate > 192000 || cfg.channels < 1 || cfg.channels > 2 ||
|
||||
cfg.block_duration_ms < 1 || cfg.block_duration_ms > 1000 || cfg.lookback_ms < 0 || cfg.lookback_ms > 5000 ||
|
||||
cfg.lookahead_ms < 0 || cfg.lookahead_ms > 5000 || !isfinite(cfg.target_level) || cfg.target_level <= 0 ||
|
||||
!isfinite(cfg.max_gain_db) || cfg.max_gain_db < 0 || cfg.max_gain_db > 60 ||
|
||||
!isfinite(cfg.rise_rate_per_sec) || cfg.rise_rate_per_sec <= 0 ||
|
||||
!isfinite(cfg.release_rate_per_sec) || cfg.release_rate_per_sec <= 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid rate=%d ch=%d block=%d back=%d ahead=%d",
|
||||
cfg.sample_rate, cfg.channels, cfg.block_duration_ms, cfg.lookback_ms, cfg.lookahead_ms);
|
||||
return -1;
|
||||
}
|
||||
int block = cfg.sample_rate * cfg.block_duration_ms / 1000 * cfg.channels;
|
||||
int back = cfg.lookback_ms / cfg.block_duration_ms, ahead = cfg.lookahead_ms / cfg.block_duration_ms;
|
||||
size_t levels_cap = (size_t)back + ahead + 2, pending_cap = (size_t)ahead + 2;
|
||||
float* levels = u_calloc(levels_cap, sizeof(float));
|
||||
int16_t* pending = u_calloc(pending_cap * (size_t)block, sizeof(int16_t));
|
||||
if (!levels || !pending) {
|
||||
u_free(levels); u_free(pending);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: buffers allocation failed");
|
||||
return -1;
|
||||
}
|
||||
u_free(ac->block_levels); u_free(ac->pending);
|
||||
ac->cfg = cfg; ac->block_samples = block;
|
||||
ac->lookback_blocks = back; ac->lookahead_blocks = ahead;
|
||||
ac->block_levels = levels; ac->levels_cap = levels_cap;
|
||||
ac->pending = pending; ac->pending_cap = pending_cap;
|
||||
ac->rise_factor_per_block = powf(cfg.rise_rate_per_sec, (float)cfg.block_duration_ms / 1000.0f);
|
||||
ac->release_factor_per_block = powf(cfg.release_rate_per_sec, (float)cfg.block_duration_ms / 1000.0f);
|
||||
ac->max_gain = powf(10.0f, cfg.max_gain_db / 20.0f);
|
||||
ac->configured = 1;
|
||||
audio_compressor_reset(ac);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "compressor: configured rate=%d ch=%d ahead=%dms history=%zu pending=%zu",
|
||||
cfg.sample_rate, cfg.channels, cfg.lookahead_ms, levels_cap, pending_cap);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int audio_compressor_is_enabled(const struct audio_compressor* ac) {
|
||||
return ac ? ac->enabled : 0;
|
||||
struct audio_compressor* audio_compressor_clone_config(const struct audio_compressor* source) {
|
||||
if (!source || !source->configured) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: cannot clone configuration"); return NULL; }
|
||||
struct audio_compressor* copy = audio_compressor_create();
|
||||
if (copy && audio_compressor_configure(copy, &source->cfg) != 0) { audio_compressor_destroy(copy); return NULL; }
|
||||
if (copy) copy->enabled = source->enabled;
|
||||
return copy;
|
||||
}
|
||||
|
||||
void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled) { if (ac) ac->enabled = !!enabled; }
|
||||
int audio_compressor_is_enabled(const struct audio_compressor* ac) { return ac && ac->enabled; }
|
||||
|
||||
/* Одна оценка для всех каналов сохраняет баланс стерео. */
|
||||
static float compute_block_level(const int16_t* samples, size_t count) {
|
||||
double sum_sq = 0.0;
|
||||
float peak = 0.0f;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
float v = (float)samples[i] / 32768.0f;
|
||||
sum_sq += (double)v * v;
|
||||
float av = fabsf(v);
|
||||
if (av > peak) peak = av;
|
||||
if (fabsf(v) > peak) peak = fabsf(v);
|
||||
}
|
||||
float rms = (float)sqrt(sum_sq / (double)count);
|
||||
return (rms * 2.0f + peak) / 2.0f;
|
||||
return (2.0f * sqrtf((float)(sum_sq / count)) + peak) / 2.0f;
|
||||
}
|
||||
|
||||
static void ensure_output_cap(struct audio_compressor* ac, size_t needed) {
|
||||
size_t want = ac->output_size + needed;
|
||||
if (want <= ac->output_cap) return;
|
||||
size_t new_cap = ac->output_cap ? ac->output_cap * 2 : 4096;
|
||||
while (new_cap < want) new_cap *= 2;
|
||||
int16_t* tmp = u_realloc(ac->output, new_cap * sizeof(int16_t));
|
||||
if (!tmp) return;
|
||||
ac->output = tmp;
|
||||
ac->output_cap = new_cap;
|
||||
}
|
||||
|
||||
static void append_output(struct audio_compressor* ac, const int16_t* samples, size_t count) {
|
||||
ensure_output_cap(ac, count);
|
||||
memcpy(ac->output + ac->output_size, samples, count * sizeof(int16_t));
|
||||
ac->output_size += count;
|
||||
}
|
||||
|
||||
static void ensure_block_levels_cap(struct audio_compressor* ac) {
|
||||
if (ac->block_levels_count < ac->block_levels_cap) return;
|
||||
size_t new_cap = ac->block_levels_cap ? ac->block_levels_cap * 2 : BLOCK_LEVELS_CHUNK;
|
||||
float* tmp = u_realloc(ac->block_levels, new_cap * sizeof(float));
|
||||
if (!tmp) return;
|
||||
ac->block_levels = tmp;
|
||||
ac->block_levels_cap = new_cap;
|
||||
}
|
||||
|
||||
static void add_block_level(struct audio_compressor* ac, float level) {
|
||||
ensure_block_levels_cap(ac);
|
||||
ac->block_levels[ac->block_levels_count++] = level;
|
||||
}
|
||||
|
||||
static void ensure_pending_cap(struct audio_compressor* ac) {
|
||||
if (ac->pending_count < ac->pending_cap) return;
|
||||
size_t new_cap = ac->pending_cap ? ac->pending_cap * 2 : PENDING_CHUNK;
|
||||
struct pending_block* tmp = u_realloc(ac->pending, new_cap * sizeof(*tmp));
|
||||
if (!tmp) return;
|
||||
ac->pending = tmp;
|
||||
ac->pending_cap = new_cap;
|
||||
}
|
||||
|
||||
static void process_pending_block(struct audio_compressor* ac);
|
||||
|
||||
void audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count) {
|
||||
if (!ac || !samples || count == 0) return;
|
||||
|
||||
if (!ac->enabled) {
|
||||
append_output(ac, samples, count);
|
||||
return;
|
||||
/* Выход записи растёт только вне аппаратного callback; OOM переводит запись в failed. */
|
||||
static int reserve_output(struct audio_compressor* ac, size_t count) {
|
||||
if (count > SIZE_MAX / sizeof(int16_t) - ac->output_size) goto failed;
|
||||
size_t want = ac->output_size + count;
|
||||
if (want <= ac->output_cap) return 0;
|
||||
size_t capacity = ac->output_cap ? ac->output_cap : 4096;
|
||||
while (capacity < want) {
|
||||
if (capacity > SIZE_MAX / sizeof(int16_t) / 2) { capacity = want; break; }
|
||||
capacity *= 2;
|
||||
}
|
||||
|
||||
size_t remaining = count;
|
||||
const int16_t* src = samples;
|
||||
|
||||
while (remaining > 0) {
|
||||
size_t need = (size_t)ac->block_samples - ac->accum_count;
|
||||
size_t take = remaining < need ? remaining : need;
|
||||
|
||||
size_t new_cnt = ac->accum_count + take;
|
||||
int16_t* tmp = u_realloc(ac->accumulator, new_cnt * sizeof(int16_t));
|
||||
if (!tmp) return;
|
||||
ac->accumulator = tmp;
|
||||
memcpy(ac->accumulator + ac->accum_count, src, take * sizeof(int16_t));
|
||||
ac->accum_count = new_cnt;
|
||||
|
||||
src += take;
|
||||
remaining -= take;
|
||||
|
||||
if (ac->accum_count == (size_t)ac->block_samples) {
|
||||
float level = compute_block_level(ac->accumulator, ac->accum_count);
|
||||
add_block_level(ac, level);
|
||||
|
||||
ensure_pending_cap(ac);
|
||||
ac->pending[ac->pending_count].samples = ac->accumulator;
|
||||
ac->pending[ac->pending_count].count = ac->accum_count;
|
||||
ac->pending[ac->pending_count].level_index = ac->block_levels_count - 1;
|
||||
ac->pending_count++;
|
||||
|
||||
ac->accumulator = NULL;
|
||||
ac->accum_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
while (ac->pending_count > (size_t)ac->lookahead_blocks)
|
||||
process_pending_block(ac);
|
||||
int16_t* samples = u_realloc(ac->output, capacity * sizeof(int16_t));
|
||||
if (!samples) goto failed;
|
||||
ac->output = samples; ac->output_cap = capacity;
|
||||
return 0;
|
||||
failed:
|
||||
ac->failed = 1;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: recording output allocation failed samples=%zu", count);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void process_pending_block(struct audio_compressor* ac) {
|
||||
if (ac->pending_count == 0) return;
|
||||
|
||||
size_t idx = ac->pending[0].level_index;
|
||||
|
||||
size_t win_start = idx >= (size_t)ac->lookback_blocks ? idx - (size_t)ac->lookback_blocks : 0;
|
||||
size_t win_end = idx + (size_t)ac->lookahead_blocks;
|
||||
if (win_end >= ac->block_levels_count) win_end = ac->block_levels_count - 1;
|
||||
|
||||
float envelope = 0.0f;
|
||||
for (size_t i = win_start; i <= win_end; i++) {
|
||||
if (ac->block_levels[i] > envelope) envelope = ac->block_levels[i];
|
||||
/* Общий алгоритм усиления для записи с lookahead и потокового кадра без lookahead. */
|
||||
static void apply_gain(struct audio_compressor* ac, int16_t* samples, size_t count, size_t idx) {
|
||||
size_t begin = idx >= (size_t)ac->lookback_blocks ? idx - ac->lookback_blocks : 0;
|
||||
size_t end = idx + ac->lookahead_blocks;
|
||||
if (end >= ac->blocks_seen) end = ac->blocks_seen - 1;
|
||||
float envelope = 0;
|
||||
for (size_t i = begin; i <= end; i++) {
|
||||
float level = ac->block_levels[i % ac->levels_cap];
|
||||
if (level > envelope) envelope = level;
|
||||
}
|
||||
if (envelope < 0.0001f) envelope = 0.0001f;
|
||||
|
||||
@@ -227,66 +148,106 @@ static void process_pending_block(struct audio_compressor* ac) {
|
||||
float G_prev = ac->gain_smoothed;
|
||||
|
||||
if (G_raw > ac->gain_smoothed) {
|
||||
/* сигнал стал тише — плавно поднимаем gain (атака) */
|
||||
float max_rise = ac->gain_smoothed * ac->rise_factor_per_block;
|
||||
ac->gain_smoothed = G_raw < max_rise ? G_raw : max_rise;
|
||||
} else {
|
||||
ac->gain_smoothed = G_raw;
|
||||
/* сигнал стал громче — плавно опускаем gain (release), без мгновенной ступеньки
|
||||
* (мгновенный дроп давал клик на границе 20мс-блока). */
|
||||
float min_fall = ac->gain_smoothed / ac->release_factor_per_block;
|
||||
ac->gain_smoothed = G_raw > min_fall ? G_raw : min_fall;
|
||||
}
|
||||
if (ac->gain_smoothed > ac->max_gain) ac->gain_smoothed = ac->max_gain;
|
||||
|
||||
float gain_db = 20.0f * log10f(ac->gain_smoothed > 0.0001f ? ac->gain_smoothed : 0.0001f);
|
||||
float prev_db = 20.0f * log10f(G_prev > 0.0001f ? G_prev : 0.0001f);
|
||||
if (ac->dbg_counter % 25 == 0 || fabsf(gain_db - prev_db) > 3.0f) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "audio_compressor: block %zu level=%.4f envelope=%.4f gainRaw=%.1fdB gain=%.1fdB",
|
||||
idx, ac->block_levels[idx], envelope, 20.0f * log10f(G_raw), gain_db);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "audio_compressor: block %zu level=%.4f envelope=%.4f gainRaw=%.1fdB gain=%.1fdB clip=%u",
|
||||
idx, ac->block_levels[idx % ac->levels_cap], envelope, 20.0f * log10f(G_raw), gain_db, (unsigned)ac->clip_count);
|
||||
}
|
||||
ac->dbg_counter++;
|
||||
|
||||
for (size_t i = 0; i < ac->pending[0].count; i++) {
|
||||
float v = (float)ac->pending[0].samples[i] * ac->gain_smoothed;
|
||||
if (v > 32767.0f) v = 32767.0f;
|
||||
if (v < -32768.0f) v = -32768.0f;
|
||||
ac->pending[0].samples[i] = (int16_t)(int)v;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
float v = (float)samples[i] * ac->gain_smoothed;
|
||||
if (v > 32767.0f) { v = 32767.0f; ac->clip_count++; }
|
||||
if (v < -32768.0f) { v = -32768.0f; ac->clip_count++; }
|
||||
samples[i] = (int16_t)(int)v;
|
||||
}
|
||||
append_output(ac, ac->pending[0].samples, ac->pending[0].count);
|
||||
u_free(ac->pending[0].samples);
|
||||
|
||||
/* shift remaining pending blocks down */
|
||||
if (ac->pending_count > 1)
|
||||
memmove(ac->pending, ac->pending + 1, (ac->pending_count - 1) * sizeof(*ac->pending));
|
||||
ac->pending_count--;
|
||||
}
|
||||
|
||||
void audio_compressor_flush(struct audio_compressor* ac) {
|
||||
if (!ac) return;
|
||||
if (!ac->enabled) return;
|
||||
/* Слить один готовый блок записи. История хранится по абсолютному номеру в кольце. */
|
||||
static int drain_block(struct audio_compressor* ac, size_t count) {
|
||||
if (reserve_output(ac, count) != 0) return -1;
|
||||
int16_t* samples = ac->pending + ac->pending_head * ac->block_samples;
|
||||
apply_gain(ac, samples, count, ac->blocks_seen - ac->pending_count);
|
||||
memcpy(ac->output + ac->output_size, samples, count * sizeof(int16_t));
|
||||
ac->output_size += count;
|
||||
ac->pending_head = (ac->pending_head + 1) % ac->pending_cap;
|
||||
--ac->pending_count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ac->accum_count > 0) {
|
||||
float level = compute_block_level(ac->accumulator, ac->accum_count);
|
||||
add_block_level(ac, level);
|
||||
|
||||
ensure_pending_cap(ac);
|
||||
ac->pending[ac->pending_count].samples = ac->accumulator;
|
||||
ac->pending[ac->pending_count].count = ac->accum_count;
|
||||
ac->pending[ac->pending_count].level_index = ac->block_levels_count - 1;
|
||||
ac->pending_count++;
|
||||
|
||||
ac->accumulator = NULL;
|
||||
ac->accum_count = 0;
|
||||
/* Ровно один кадр, без output-накопителя и выделений памяти. in/out могут совпадать. */
|
||||
int audio_compressor_process_frame(struct audio_compressor* ac, const int16_t* in, int16_t* out, size_t count) {
|
||||
if (!ac || !ac->configured || ac->failed || !in || !out || count != (size_t)ac->block_samples ||
|
||||
ac->lookahead_blocks || ac->pending_count || ac->accum_count) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid streaming frame count=%zu", count);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ac->pending_count > 0)
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "audio_compressor_flush: %zu pending blocks (gainSmoothed=%.1fdB)",
|
||||
ac->pending_count, 20.0f * log10f(ac->gain_smoothed > 0.0001f ? ac->gain_smoothed : 0.0001f));
|
||||
|
||||
while (ac->pending_count > 0)
|
||||
process_pending_block(ac);
|
||||
memmove(out, in, count * sizeof(int16_t));
|
||||
if (ac->enabled) {
|
||||
size_t idx = ac->blocks_seen++;
|
||||
ac->block_levels[idx % ac->levels_cap] = compute_block_level(in, count);
|
||||
apply_gain(ac, out, count, idx);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int16_t* audio_compressor_output(const struct audio_compressor* ac) {
|
||||
return ac ? ac->output : NULL;
|
||||
int audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count) {
|
||||
if (!ac || !ac->configured || ac->failed || (!samples && count)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid recording input"); return -1;
|
||||
}
|
||||
if (!ac->enabled) {
|
||||
if (reserve_output(ac, count) != 0) return -1;
|
||||
if (count) memcpy(ac->output + ac->output_size, samples, count * sizeof(int16_t));
|
||||
ac->output_size += count;
|
||||
return 0;
|
||||
}
|
||||
while (count) {
|
||||
size_t take = (size_t)ac->block_samples - ac->accum_count;
|
||||
if (take > count) take = count;
|
||||
int16_t* block = ac->pending + ((ac->pending_head + ac->pending_count) % ac->pending_cap) * ac->block_samples;
|
||||
memcpy(block + ac->accum_count, samples, take * sizeof(int16_t));
|
||||
samples += take; count -= take; ac->accum_count += take;
|
||||
if (ac->accum_count == (size_t)ac->block_samples) {
|
||||
ac->block_levels[ac->blocks_seen++ % ac->levels_cap] = compute_block_level(block, ac->accum_count);
|
||||
ac->accum_count = 0; ++ac->pending_count;
|
||||
if (ac->pending_count > (size_t)ac->lookahead_blocks && drain_block(ac, ac->block_samples) != 0) return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t audio_compressor_output_size(const struct audio_compressor* ac) {
|
||||
return ac ? ac->output_size : 0;
|
||||
int audio_compressor_flush(struct audio_compressor* ac) {
|
||||
if (!ac || !ac->configured || ac->failed) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid flush"); return -1; }
|
||||
if (!ac->enabled) return 0;
|
||||
size_t tail = ac->accum_count;
|
||||
if (tail) {
|
||||
int16_t* block = ac->pending + ((ac->pending_head + ac->pending_count) % ac->pending_cap) * ac->block_samples;
|
||||
ac->block_levels[ac->blocks_seen++ % ac->levels_cap] = compute_block_level(block, tail);
|
||||
ac->accum_count = 0; ++ac->pending_count;
|
||||
}
|
||||
while (ac->pending_count) {
|
||||
size_t count = tail && ac->pending_count == 1 ? tail : (size_t)ac->block_samples;
|
||||
if (drain_block(ac, count) != 0) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int16_t* audio_compressor_output(const struct audio_compressor* ac) { return ac ? ac->output : NULL; }
|
||||
size_t audio_compressor_output_size(const struct audio_compressor* ac) { return ac ? ac->output_size : 0; }
|
||||
void audio_compressor_clear_output(struct audio_compressor* ac) { if (ac) ac->output_size = 0; }
|
||||
uint32_t audio_compressor_clip_count(const struct audio_compressor* ac) { return ac ? ac->clip_count : 0; }
|
||||
float audio_compressor_gain_db(const struct audio_compressor* ac) {
|
||||
return ac && ac->configured && ac->enabled ? 20.0f * log10f(ac->gain_smoothed) : 0.0f;
|
||||
}
|
||||
+26
-4
@@ -1,3 +1,9 @@
|
||||
/* audio_compressor — адаптивное усиление int16 PCM (моно/стерео) с общей огибающей каналов.
|
||||
* create → configure → обработка → destroy; объект использует один владелец, блокировок нет.
|
||||
* Запись: push принимает произвольные порции, flush завершает хвост и lookahead; output принадлежит объекту.
|
||||
* Поток: process_frame обрабатывает ровно sample_rate*block_duration_ms/1000*channels отсчётов,
|
||||
* требует нулевого lookahead и пустого входного накопителя; in/out могут совпадать.
|
||||
* count/output_size — interleaved отсчёты, не байты и не кадры на канал. Выключение даёт исходный PCM. */
|
||||
#ifndef AUDIO_COMPRESSOR_H
|
||||
#define AUDIO_COMPRESSOR_H
|
||||
|
||||
@@ -18,24 +24,40 @@ typedef struct {
|
||||
int lookahead_ms;
|
||||
float max_gain_db;
|
||||
float rise_rate_per_sec;
|
||||
float target_level;
|
||||
float release_rate_per_sec; /* скорость плавного снижения gain (0 = авто: rise*5) */
|
||||
float target_level; /* Линейная амплитуда огибающей, не дБ; 0 задаёт default 0.25. */
|
||||
} audio_compressor_config_t;
|
||||
|
||||
struct audio_compressor* audio_compressor_create(void);
|
||||
/* Новый пустой compressor с теми же настройками; позволяет передать запись worker без копии PCM. */
|
||||
struct audio_compressor* audio_compressor_clone_config(const struct audio_compressor* source);
|
||||
void audio_compressor_destroy(struct audio_compressor* ac);
|
||||
|
||||
void audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg);
|
||||
int audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg);
|
||||
/* Потоковый кадр без lookahead: результат немедленный, память не выделяется; 0 / -1. */
|
||||
int audio_compressor_process_frame(struct audio_compressor* ac, const int16_t* in, int16_t* out, size_t count);
|
||||
void audio_compressor_reset(struct audio_compressor* ac);
|
||||
|
||||
void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled);
|
||||
int audio_compressor_is_enabled(const struct audio_compressor* ac);
|
||||
|
||||
void audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count);
|
||||
void audio_compressor_flush(struct audio_compressor* ac);
|
||||
/* push копирует вход; push/flush возвращают 0 / -1. Ошибка накопления требует reset перед повтором. */
|
||||
int audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count);
|
||||
int audio_compressor_flush(struct audio_compressor* ac);
|
||||
|
||||
/* Указатель действителен до изменения/освобождения выходного буфера; вызывающий его не освобождает. */
|
||||
const int16_t* audio_compressor_output(const struct audio_compressor* ac);
|
||||
size_t audio_compressor_output_size(const struct audio_compressor* ac);
|
||||
|
||||
/* Сбросить накопленный выход (output_size=0) после слива — для потокового режима.
|
||||
* НЕ трогает gain_smoothed (адаптивная громкость сохраняется). */
|
||||
void audio_compressor_clear_output(struct audio_compressor* ac);
|
||||
|
||||
/* Число сэмплов, обрезанных по потолку int16 (клиппинг) с момента create/reset. */
|
||||
uint32_t audio_compressor_clip_count(const struct audio_compressor* ac);
|
||||
/* Текущее фактическое усиление в дБ; выключенный compressor возвращает 0. */
|
||||
float audio_compressor_gain_db(const struct audio_compressor* ac);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+42
-28
@@ -132,6 +132,12 @@ static const struct {
|
||||
{"video", DEBUG_CATEGORY_VIDEO},
|
||||
{"reality", DEBUG_CATEGORY_REALITY},
|
||||
{"dm", DEBUG_CATEGORY_DM},
|
||||
{"call", DEBUG_CATEGORY_CALL},
|
||||
{"radio", DEBUG_CATEGORY_RADIO},
|
||||
{"vad", DEBUG_CATEGORY_VAD},
|
||||
{"aec", DEBUG_CATEGORY_AEC},
|
||||
{"stream", DEBUG_CATEGORY_STREAM},
|
||||
{"discovery", DEBUG_CATEGORY_DISCOVERY},
|
||||
{"all", DEBUG_CATEGORY_ALL},
|
||||
{NULL, DEBUG_CATEGORY_NONE}
|
||||
};
|
||||
@@ -266,6 +272,10 @@ void debug_set_masks(debug_category_t category_idx, debug_level_t level) {
|
||||
debug_set_category_level(category_idx, level);
|
||||
}
|
||||
|
||||
void debug_enable_timestamp(int enable) {
|
||||
g_debug_config.timestamp_enabled = !!enable;
|
||||
}
|
||||
|
||||
void debug_enable_function_name(int enable) {
|
||||
g_debug_config.function_name_enabled = enable;
|
||||
}
|
||||
@@ -317,15 +327,24 @@ void debug_disable_file_output(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Reopen log file (append mode) for SIGHUP. Complete operation with full error logging. */
|
||||
/* Переоткрыть лог в append-режиме; при ошибке открытия сохранить прежний поток. */
|
||||
int debug_reopen_log(void) {
|
||||
if (!g_debug_config.output_file) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "No log path for reopen");
|
||||
return -1;
|
||||
}
|
||||
const char* path = g_debug_config.output_file;
|
||||
debug_disable_file_output();
|
||||
debug_enable_file_output(path, 0); // append
|
||||
FILE* new_file = fopen(path, "a");
|
||||
if (!new_file) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot reopen log '%s': %s", path, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
FILE* old_file = g_debug_config.file_output;
|
||||
g_debug_config.file_output = new_file;
|
||||
if (old_file && fclose(old_file) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot close previous log '%s': %s", path, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Log reopened: %s", path);
|
||||
return 0;
|
||||
}
|
||||
@@ -385,13 +404,15 @@ void debug_output(debug_level_t level, debug_category_t category_idx,
|
||||
va_start(args, format);
|
||||
|
||||
/* Add timestamp with microseconds: hh:mm:ss-xxx.yyy */
|
||||
struct timeval tv;
|
||||
utun_gettimeofday(&tv, NULL);
|
||||
time_t tv_sec = (time_t)tv.tv_sec;
|
||||
struct tm* tm_info = localtime(&tv_sec);
|
||||
char time_str[32];
|
||||
strftime(time_str, sizeof(time_str), "%H:%M:%S", tm_info);
|
||||
BUF_ADVANCE(snprintf(buffer + offset, remaining, "[%s-%03ld.%03ld] ", time_str, tv.tv_usec / 1000, tv.tv_usec % 1000));
|
||||
if (g_debug_config.timestamp_enabled) {
|
||||
struct timeval tv;
|
||||
utun_gettimeofday(&tv, NULL);
|
||||
time_t tv_sec = (time_t)tv.tv_sec;
|
||||
struct tm* tm_info = localtime(&tv_sec);
|
||||
char time_str[32];
|
||||
strftime(time_str, sizeof(time_str), "%H:%M:%S", tm_info);
|
||||
BUF_ADVANCE(snprintf(buffer + offset, remaining, "[%s-%03ld.%03ld] ", time_str, tv.tv_usec / 1000, tv.tv_usec % 1000));
|
||||
}
|
||||
|
||||
/* Add thread marker if set */
|
||||
if (g_debug_config.thread_marker > 0) {
|
||||
@@ -454,7 +475,7 @@ void debug_set_log_hook(debug_log_hook_fn hook) {
|
||||
g_log_hook = hook;
|
||||
}
|
||||
|
||||
/* Parse debug configuration from string (e.g., "ll_queue:debug,uasync:info,memory:warn,etcp:error") */
|
||||
/* Настройка отдельных категорий: "sys=debug,etcp=error"; разделитель ':' также допустим. */
|
||||
int debug_parse_config(const char* config_string) {
|
||||
if (!config_string || !*config_string) {
|
||||
return 0;
|
||||
@@ -462,12 +483,10 @@ int debug_parse_config(const char* config_string) {
|
||||
|
||||
char* str = u_strdup(config_string);
|
||||
if (!str) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to allocate debug configuration copy");
|
||||
return -1;
|
||||
}
|
||||
|
||||
debug_category_t categories = 0;
|
||||
debug_level_t level = DEBUG_LEVEL_NONE;
|
||||
|
||||
char* token = strtok(str, ",");
|
||||
while (token) {
|
||||
/* Trim leading spaces */
|
||||
@@ -475,15 +494,16 @@ int debug_parse_config(const char* config_string) {
|
||||
token++;
|
||||
}
|
||||
|
||||
char* colon = strchr(token, ':');
|
||||
if (!colon) {
|
||||
char* separator = strpbrk(token, "=:");
|
||||
if (!separator) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Missing '=' or ':' in debug setting '%s'", token);
|
||||
u_free(str);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*colon = '\0';
|
||||
*separator = '\0';
|
||||
char* category_name = token;
|
||||
char* level_name = colon + 1;
|
||||
char* level_name = separator + 1;
|
||||
|
||||
/* Trim leading spaces in level_name */
|
||||
while (isspace(*level_name)) {
|
||||
@@ -491,22 +511,16 @@ int debug_parse_config(const char* config_string) {
|
||||
}
|
||||
|
||||
debug_category_t cat = get_category_by_name(category_name);
|
||||
if (cat == DEBUG_CATEGORY_NONE) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Unknown debug category '%s'", category_name);
|
||||
if (cat <= DEBUG_CATEGORY_NONE || cat >= DEBUG_CATEGORY_COUNT) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Unknown or unsupported debug category '%s' (expected an individual category)", category_name);
|
||||
u_free(str);
|
||||
return -1;
|
||||
}
|
||||
|
||||
debug_level_t lev = debug_level_from_name(level_name);
|
||||
|
||||
/* Set per-category level */
|
||||
int idx = 0;
|
||||
while (!(cat & (1ULL << idx)) && idx < DEBUG_CATEGORY_COUNT) {
|
||||
idx++;
|
||||
}
|
||||
if (idx < DEBUG_CATEGORY_COUNT) {
|
||||
g_debug_config.category_levels[idx] = lev;
|
||||
}
|
||||
debug_set_category_level(cat, lev);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Applied debug setting: %s=%s (category=%d, level=%d)", category_name, level_name, cat, lev);
|
||||
|
||||
token = strtok(NULL, ",");
|
||||
}
|
||||
|
||||
+49
-24
@@ -1,6 +1,12 @@
|
||||
/**
|
||||
* Runtime debug configuration system
|
||||
* Provides flexible control over debug output without recompilation
|
||||
* Управление диагностикой: debug_config_init → уровни категорий и настройки вывода → DEBUG_*.
|
||||
* NONE наследует общий уровень, DISABLED отключает категорию. Имена настроек — category=level.
|
||||
* Открытый файл имеет приоритет над консолью; hook получает сообщения независимо от выбора выхода.
|
||||
* Уровни: disabled, none, error, warn, info, debug, trace; отдельный уровень категории заменяет общий, не max().
|
||||
* Пример: debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); debug_parse_config("etcp=trace,config=info").
|
||||
* DEBUG_* фильтруют до форматирования; прямой debug_output фильтр не проверяет.
|
||||
* Строка с префиксами ограничена буфером 4096 байт и обрезается; каждый вывод fflush.
|
||||
* Модуль и смена глобальных настроек/файлов не синхронизированы; параллельный доступ согласует владелец.
|
||||
*/
|
||||
|
||||
#ifndef DEBUG_CONFIG_H
|
||||
@@ -70,19 +76,25 @@ typedef int debug_category_t;
|
||||
#define DEBUG_CATEGORY_VIDEO 29 // Video module (probe/transcode)
|
||||
#define DEBUG_CATEGORY_REALITY 30 // REALITY TLS camouflage
|
||||
#define DEBUG_CATEGORY_DM 31 // DM — direct p2p chat between two users
|
||||
#define DEBUG_CATEGORY_COUNT 32 // Total number of categories
|
||||
#define DEBUG_CATEGORY_CALL 32 // Call — P2P audio call (signaling + media)
|
||||
#define DEBUG_CATEGORY_RADIO 33 // Radio — PTT walkie-talkie multicast (voice)
|
||||
#define DEBUG_CATEGORY_VAD 34 // VAD — Silero voice activity detector (ONNX Runtime)
|
||||
#define DEBUG_CATEGORY_AEC 35 // AEC — acoustic echo cancellation (SpeexDSP)
|
||||
#define DEBUG_CATEGORY_STREAM 36 // Подписанные потоки, FEC, буферы и выбор поставщиков
|
||||
#define DEBUG_CATEGORY_DISCOVERY 37 // LAN UDP: discovery and delivery
|
||||
#define DEBUG_CATEGORY_COUNT 38 // Total number of categories
|
||||
#define DEBUG_CATEGORY_ALL (-1) // special value for all categories
|
||||
|
||||
/* Debug configuration structure */
|
||||
typedef struct {
|
||||
debug_level_t level; // Global debug level (default: ERROR)
|
||||
debug_level_t category_levels[DEBUG_CATEGORY_COUNT]; // Per-category levels (0 = disabled)
|
||||
debug_level_t category_levels[DEBUG_CATEGORY_COUNT]; // Уровень категории: NONE наследует общий, DISABLED выключает
|
||||
int timestamp_enabled; // Include timestamps in output
|
||||
int function_name_enabled; // Include function names
|
||||
int file_line_enabled; // Include file:line info
|
||||
const char* output_file; // NULL = stdout, otherwise file path
|
||||
const char* output_file; // Заимствованный путь для reopen; строка должна жить, пока поле её хранит
|
||||
|
||||
// Dual output support
|
||||
// Выход: файл, иначе консоль при console_enabled != 0
|
||||
FILE* file_output; // File handle for log file (NULL if not open)
|
||||
int console_enabled; // Enable console output
|
||||
int thread_marker; // Thread identification (0=none, e.g. 2 for uasync)
|
||||
@@ -91,38 +103,47 @@ typedef struct {
|
||||
/* Global debug configuration */
|
||||
extern debug_config_t g_debug_config;
|
||||
|
||||
/* Initialize debug system with default settings */
|
||||
/* До использования: ERROR, категории NONE, консоль stdout, timestamp/function/file:line включены.
|
||||
* Повторный init сбрасывает поля, но не закрывает уже открытый файл; перед ним закрыть файл отдельно. */
|
||||
void debug_config_init(void);
|
||||
|
||||
/* Set debug level */
|
||||
// итоговый level = max (global level - здесь задается, category level)
|
||||
// Общий уровень применяется к категориям с уровнем NONE.
|
||||
void debug_set_level(debug_level_t level);
|
||||
|
||||
/* Set debug level for specific category (0 = disabled, otherwise uses that level) */
|
||||
/* Уровень категории заменяет общий; NONE наследует его, DISABLED подавляет вывод. */
|
||||
void debug_set_category_level(debug_category_t category, debug_level_t level);
|
||||
|
||||
/* Enable/disable specific categories */
|
||||
/* enable фиксирует текущий общий уровень; disable возвращает к наследованию (NONE).
|
||||
* Для полного выключения использовать debug_set_category_level(cat, DEBUG_LEVEL_DISABLED). */
|
||||
void debug_enable_category(debug_category_t category);
|
||||
void debug_disable_category(debug_category_t category);
|
||||
void debug_set_categories(debug_category_t categories);
|
||||
|
||||
/* Set masks directly */
|
||||
/* Установка уровня одной категории (аргумент categories — индекс, не битовая маска). */
|
||||
void debug_set_masks(debug_category_t categories, debug_level_t level);
|
||||
|
||||
/* Configure output options */
|
||||
/* Configure output options */
|
||||
/* Ненулевое enable включает временную метку, 0 отключает. */
|
||||
void debug_enable_timestamp(int enable);
|
||||
void debug_enable_function_name(int enable);
|
||||
void debug_enable_file_line(int enable);
|
||||
void debug_set_output_file(const char* file_path);
|
||||
/* Открыть ненулевой file_path в append-режиме, только если файл ещё не открыт; путь заимствован. */
|
||||
void debug_set_output_file(const char* file_path);
|
||||
|
||||
/* Dual output configuration */
|
||||
/* Заменить файл: truncate != 0 → обрезать, иначе append. NULL ничего не меняет.
|
||||
* Путь заимствован; успешное открытие также выключает console_enabled. */
|
||||
void debug_enable_file_output(const char* file_path, int truncate);
|
||||
/* Закрыть файл; прежнее значение console_enabled не восстанавливается. */
|
||||
void debug_disable_file_output(void);
|
||||
/* Переоткрыть текущий путь в append-режиме: 0 — успех, -1 — ошибка с диагностикой.
|
||||
* При ошибке открытия сохраняет прежний поток; console_enabled не меняет. */
|
||||
int debug_reopen_log(void);
|
||||
/* Вывод в консоль разрешён только при отсутствии открытого файла. */
|
||||
void debug_enable_console(int enable);
|
||||
|
||||
|
||||
// IP address to string (static buffer, single-threaded)
|
||||
// IP/sockaddr в строку: результат возвращается по значению, освобождать не требуется.
|
||||
typedef struct {
|
||||
char str[54]; // INET6_ADDRSTRLEN(45) + 6chars (:port) + \0
|
||||
} ip_str_t;
|
||||
@@ -130,7 +151,7 @@ typedef struct {
|
||||
ip_str_t ip_to_str(const void *addr, int family);// big endian addr
|
||||
ip_str_t sockaddr_storage_to_str(const struct sockaddr_storage *addr);// автоопределение v4 или v6
|
||||
|
||||
// hex dump в лог
|
||||
// Hex dump: первые min(len, 128) байт; при len > 128 добавляет "...". Применяется фильтр уровня/категории.
|
||||
void log_dump(int level, int category, const char* prefix, const uint8_t* data, size_t len);
|
||||
|
||||
/* Check if debug output should be shown for given level and category */
|
||||
@@ -139,13 +160,13 @@ int debug_should_output(debug_level_t level, debug_category_t category_idx);
|
||||
/* Get current debug level */
|
||||
debug_level_t debug_get_level(void);
|
||||
|
||||
/* Get category name by index */
|
||||
/* Заимствованное имя категории; для неизвестного индекса возвращает "UNKNOWN". */
|
||||
const char* debug_get_category_name(debug_category_t category_idx);
|
||||
|
||||
/* Get category by name string */
|
||||
/* Поиск без учёта регистра: индекс, ALL для "all", NONE при NULL/неизвестном имени. */
|
||||
debug_category_t get_category_by_name(const char* name);
|
||||
|
||||
/* Get level by name string with error reporting */
|
||||
/* Поиск без учёта регистра: NULL → NONE; неизвестное имя логируется и даёт INFO (fallback). */
|
||||
debug_level_t debug_level_from_name(const char* name);
|
||||
|
||||
/* Set category level using text names (main API) */
|
||||
@@ -157,16 +178,20 @@ int debug_apply_category_config(const char* category_name, const char* level_nam
|
||||
/* Apply global debug level from text name */
|
||||
void debug_apply_global_level(const char* level_name);
|
||||
|
||||
/* Format and output debug message (internal use by macros) */
|
||||
/* Форматирует сообщение printf-формата с необязательными метками; фильтрацию выполняет вызывающий.
|
||||
* Время календарное, локальное, hh:mm:ss-миллисекунды.микросекунды; положительный thread_marker выводится отдельно. */
|
||||
void debug_output(debug_level_t level, debug_category_t category_idx,
|
||||
const char* function, const char* file, int line,
|
||||
const char* format, ...);
|
||||
|
||||
/* Log hook — if set, called with every debug message (for JNI/bridge integration) */
|
||||
/* Hook вызывается синхронно для форматированного сообщения до записи в выход; NULL снимает hook.
|
||||
* category/message заимствованы на время вызова, message указывает на стековый буфер; для хранения скопировать. */
|
||||
typedef void (*debug_log_hook_fn)(int level, const char* category, const char* message);
|
||||
void debug_set_log_hook(debug_log_hook_fn hook);
|
||||
|
||||
/* Parse debug configuration from string (e.g. "etcp=trace,config=info") */
|
||||
/* Настройка отдельных категорий: "etcp=trace,config=info" (также ':' вместо '=').
|
||||
* NULL/пустая строка ничего не меняет. Возвращает 0 при успехе, -1 при ошибке;
|
||||
* настройки до ошибочного элемента остаются применёнными. */
|
||||
int debug_parse_config(const char* config_string);
|
||||
|
||||
/* Convenience macros for debug output */
|
||||
@@ -195,8 +220,8 @@ int debug_parse_config(const char* config_string);
|
||||
debug_output(DEBUG_LEVEL_TRACE, category, __FUNCTION__, __FILE__, __LINE__, fmt, ##__VA_ARGS__); \
|
||||
} } while(0)
|
||||
|
||||
/* Backward compatibility - default to ERROR level if not specified */
|
||||
#define DEBUG_OUTPUT(fmt, ...) DEBUG_ERROR(DEBUG_CATEGORY_ALL, fmt, ##__VA_ARGS__)
|
||||
/* Сообщение без явной категории: ERROR в категории GENERAL, с обычной фильтрацией. */
|
||||
#define DEBUG_OUTPUT(fmt, ...) DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, fmt, ##__VA_ARGS__)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
# debug_config — Runtime-система отладочного логирования
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
Модуль обеспечивает гибкое управление отладочным выводом без перекомпиляции. Позволяет:
|
||||
- Задавать **глобальный уровень** логирования (error/warn/info/debug/trace).
|
||||
- **Покатегорийно** включать/выключать или переопределять уровень (26 категорий: etcp, crypto, tun, routing, bgp, bbr, …).
|
||||
- Вести **двойной вывод**: в консоль (stdout) и/или в файл.
|
||||
- Настраивать формат сообщений (временные метки с микросекундами, имя функции, файл:строка).
|
||||
- Парсить конфигурационную строку вида `"etcp=trace,config=info"` из конфиг-файла.
|
||||
|
||||
Логика вывода: итоговый уровень = **max(глобальный уровень, уровень категории)**. Сообщение печатается, если его уровень ≤ итогового.
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
### 2.1. Инициализация
|
||||
|
||||
```c
|
||||
#include "debug_config.h"
|
||||
|
||||
debug_config_init(); // уровень по умолчанию: ERROR, консоль включена
|
||||
debug_set_level(DEBUG_LEVEL_INFO); // поднять глобальный уровень до INFO
|
||||
debug_enable_file_output("/tmp/utun.log", 1); // писать в файл (truncate=1)
|
||||
debug_apply_category_config("etcp", "trace"); // для ETCP-категории — всё
|
||||
```
|
||||
|
||||
### 2.2. Макросы логирования
|
||||
|
||||
```c
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "Decrypt failed: len=%d", len);
|
||||
DEBUG_WARN(DEBUG_CATEGORY_ETCP, "Retransmit timeout, seq=%u", seq);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "Socket created on port %d", port);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_ROUTING, "Route added: %s -> %s", dst, gw);
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_TUN, "Packet recv: %zu bytes", n);
|
||||
```
|
||||
|
||||
Макросы делают проверку `debug_should_output()` **перед** вызовом `debug_output()`, поэтому дорогой `vsnprintf` не вызывается, если вывод не нужен.
|
||||
|
||||
### 2.3. Конфигурационная строка
|
||||
|
||||
Формат: `"категория=уровень,категория=уровень,..."`. Парсится вызовом `debug_parse_config()`. Пример из конфиг-файла:
|
||||
|
||||
```ini
|
||||
debug = etcp=trace,config=info,crypto=error
|
||||
```
|
||||
|
||||
Категории: `none, uasync, ll_queue, connection, etcp, crypto, memory, timing, config, tun, routing, timers, normalizer, bgp, socket, control, dump, traffic, debug, general, nat, keepalive, etcp_route, bbr, etcp_dump, connectivity, all`.
|
||||
|
||||
Уровни: `disabled, none, error, warn, info, debug, trace`.
|
||||
|
||||
**Важно:** старый формат `ll_queue:debug` (двоеточие) тоже работает через `debug_parse_config`, но в конфиге рекомендуется `=`.
|
||||
|
||||
### 2.4. Двойной вывод (консоль + файл)
|
||||
|
||||
- По умолчанию вывод в **stdout**.
|
||||
- `debug_enable_file_output(path, truncate)` — перенаправляет вывод в файл, консоль отключается.
|
||||
- `debug_disable_file_output()` — закрывает файл.
|
||||
- `debug_reopen_log()` — переоткрывает лог-файл (используется по SIGHUP для ротации).
|
||||
- `debug_enable_console(1/0)` — ручное управление консольным выводом.
|
||||
|
||||
### 2.5. Вспомогательные утилиты
|
||||
|
||||
```c
|
||||
log_dump(DEBUG_LEVEL_TRACE, DEBUG_CATEGORY_DUMP, "payload", data, len); // hex-дамп (до 128 байт)
|
||||
|
||||
ip_str_t s = sockaddr_storage_to_str(&addr); // "192.168.1.1:8080" или "[::1]:443"
|
||||
ip_str_t s = ip_to_str(&addr, AF_INET); // "192.168.1.1" (без порта)
|
||||
```
|
||||
|
||||
### 2.6. Нюансы
|
||||
|
||||
- **Потокобезопасность:** модуль не потокобезопасен. Несколько потоков, одновременно вызывающих макросы, могут перемешать вывод (но не упадут). Файловый вывод использует `fflush()` после каждой строки.
|
||||
- **Буфер:** размер строки лога ограничен 4096 байт (`BUFFER_SIZE`), длинные сообщения обрезаются.
|
||||
- **`debug_set_output_file`** против `debug_enable_file_output`: первая вызывается один раз (кто первый — того и тапки), вторая позволяет переоткрыть файл.
|
||||
- **Флаг `-f` (foreground):** в foreground-режиме вывод идёт в stdout, в daemon-режиме — в файл.
|
||||
|
||||
## 3. API
|
||||
|
||||
| Функция/макрос | Назначение |
|
||||
|---|---|
|
||||
| `debug_config_init()` | Инициализация: глобальный уровень ERROR, консоль включена, метки времени/функций/файлов — да |
|
||||
| `debug_set_level(level)` | Задать глобальный уровень (ERROR..TRACE) |
|
||||
| `debug_set_category_level(cat, level)` | Задать уровень конкретной категории (0 = использовать глобальный) |
|
||||
| `debug_set_category_level_by_name("etcp", "trace")` | То же, но именами строк |
|
||||
| `debug_apply_category_config("etcp", "trace")` | Установить уровень категории с логированием результата |
|
||||
| `debug_apply_global_level("info")` | Установить глобальный уровень по имени строки |
|
||||
| `debug_parse_config("etcp=trace,config=info")` | Разобрать конфигурационную строку |
|
||||
| `debug_should_output(level, cat)` | Проверить, нужно ли выводить сообщение данного уровня/категории |
|
||||
| `debug_output(level, cat, func, file, line, fmt, ...)` | Форматировать и вывести сообщение (вызывается макросами) |
|
||||
| `debug_enable_file_output(path, truncate)` | Включить вывод в файл (консоль отключается) |
|
||||
| `debug_disable_file_output()` | Закрыть файл вывода |
|
||||
| `debug_reopen_log()` | Переоткрыть лог-файл (для SIGHUP/ротации) |
|
||||
| `debug_enable_console(1/0)` | Включить/выключить консольный вывод |
|
||||
| `debug_get_level()` | Получить текущий глобальный уровень |
|
||||
| `get_category_by_name("etcp")` | Получить ID категории по имени строки |
|
||||
| `debug_level_from_name("trace")` | Получить уровень по имени строки |
|
||||
| `debug_get_category_name(cat)` | Получить имя категории по ID |
|
||||
| `log_dump(level, cat, prefix, data, len)` | Hex-дамп данных (первые 128 байт) в лог |
|
||||
| `ip_to_str(addr, family)` | IP-адрес → строка (без порта). Статический буфер |
|
||||
| `sockaddr_storage_to_str(addr)` | sockaddr_storage → "ip:port" (IPv6 в скобках). Статический буфер |
|
||||
| `DEBUG_ERROR/WARN/INFO/DEBUG/TRACE(cat, fmt, ...)` | Макросы логирования с проверкой `debug_should_output()` |
|
||||
| `DEBUG_OUTPUT(fmt, ...)` | Устаревший макрос совместимости (ERROR, категория ALL) |
|
||||
|
||||
**Глобальная переменная:** `g_debug_config` (тип `debug_config_t`) — хранит всё состояние системы логирования.
|
||||
|
||||
**Структура `debug_config_t`:**
|
||||
- `level` — глобальный уровень (по умолчанию ERROR)
|
||||
- `category_levels[26]` — уровни по категориям (NONE = использовать глобальный, DISABLED = никогда не выводить)
|
||||
- `timestamp_enabled` / `function_name_enabled` / `file_line_enabled` — формат вывода
|
||||
- `file_output` — FILE* файла (NULL если вывод в консоль)
|
||||
- `console_enabled` — вывод в stdout
|
||||
- `thread_marker` — числовой маркер потока (0 = нет)
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
#include "file_io.h"
|
||||
#include "platform_compat.h"
|
||||
#include "debug_config.h"
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <limits.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
/* Общие пути API — UTF-8; ANSI Win32 API не применяется. */
|
||||
int file_io_wide(const char *path, wchar_t wide[FILE_IO_PATH_MAX]) {
|
||||
if (path && *path && strlen(path) < FILE_IO_PATH_MAX
|
||||
&& MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, wide, FILE_IO_PATH_MAX)) return 0;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file invalid UTF-8/oversized path winerr=%lu", GetLastError());
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
int file_io_inspect(const char *path, int follow, struct file_io_info *info) {
|
||||
if (!path || !*path || strlen(path) >= FILE_IO_PATH_MAX || !info) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file inspect invalid path/output"); return -1;
|
||||
}
|
||||
struct file_io_info result = {0};
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(path, wide)) return -1;
|
||||
DWORD attributes = GetFileAttributesW(wide);
|
||||
if (attributes == INVALID_FILE_ATTRIBUTES) {
|
||||
DWORD error = GetLastError();
|
||||
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) return 1;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file inspect failed path=%s winerr=%lu", path, error); return -1;
|
||||
}
|
||||
if (!follow && (attributes & FILE_ATTRIBUTE_REPARSE_POINT)) result.type = FILE_IO_LINK;
|
||||
else {
|
||||
HANDLE handle = CreateFileW(wide, FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
|
||||
BY_HANDLE_FILE_INFORMATION data;
|
||||
int valid = handle != INVALID_HANDLE_VALUE && GetFileInformationByHandle(handle, &data);
|
||||
if (valid) {
|
||||
result.type = GetFileType(handle) != FILE_TYPE_DISK ? FILE_IO_OTHER
|
||||
: (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? FILE_IO_DIRECTORY : FILE_IO_REGULAR;
|
||||
uint64_t size = ((uint64_t)data.nFileSizeHigh << 32) | data.nFileSizeLow;
|
||||
valid = size <= INT64_MAX;
|
||||
result.size = (int64_t)size;
|
||||
result.identity[0] = data.dwVolumeSerialNumber;
|
||||
result.identity[1] = ((uint64_t)data.nFileIndexHigh << 32) | data.nFileIndexLow;
|
||||
}
|
||||
DWORD error = GetLastError();
|
||||
if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle);
|
||||
if (!valid) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file handle inspect failed path=%s winerr=%lu", path, error); return -1; }
|
||||
}
|
||||
#else
|
||||
struct stat data;
|
||||
if ((follow ? stat(path, &data) : lstat(path, &data))) {
|
||||
if (errno == ENOENT) return 1;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file inspect failed path=%s errno=%d", path, errno); return -1;
|
||||
}
|
||||
result.type = S_ISREG(data.st_mode) ? FILE_IO_REGULAR : S_ISDIR(data.st_mode) ? FILE_IO_DIRECTORY
|
||||
: S_ISLNK(data.st_mode) ? FILE_IO_LINK : FILE_IO_OTHER;
|
||||
if (data.st_size < 0 || (uint64_t)data.st_size > INT64_MAX) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file size out of range path=%s", path); return -1;
|
||||
}
|
||||
result.size = (int64_t)data.st_size;
|
||||
result.identity[0] = data.st_dev; result.identity[1] = data.st_ino;
|
||||
#endif
|
||||
*info = result;
|
||||
return 0;
|
||||
}
|
||||
|
||||
FILE *file_io_open_read(const char *path) {
|
||||
struct file_io_info info;
|
||||
if (file_io_inspect(path, 1, &info) || info.type != FILE_IO_REGULAR) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file source is not regular/readable path=%s", path ? path : "(null)"); return NULL;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(path, wide)) return NULL;
|
||||
FILE *file = _wfopen(wide, L"rb");
|
||||
#else
|
||||
FILE *file = fopen(path, "rb");
|
||||
#endif
|
||||
if (!file) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file open read failed path=%s errno=%d", path, errno);
|
||||
return file;
|
||||
}
|
||||
|
||||
int file_io_paths(const char *source, const char *destination) {
|
||||
struct file_io_info input, output;
|
||||
if (file_io_inspect(source, 1, &input) || input.type != FILE_IO_REGULAR) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file invalid source path=%s", source ? source : "(null)"); return -1;
|
||||
}
|
||||
int status = file_io_inspect(destination, 0, &output);
|
||||
if (status == 1) return 0;
|
||||
if (status < 0) return -1;
|
||||
if (output.type != FILE_IO_REGULAR) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file destination is not regular path=%s type=%d", destination, output.type); return -1;
|
||||
}
|
||||
return !memcmp(input.identity, output.identity, sizeof(input.identity));
|
||||
}
|
||||
|
||||
int file_io_temp_open(struct file_io_temp *output, const char *destination) {
|
||||
if (!output || output->file || output->temporary[0] || !destination || !*destination
|
||||
|| strlen(destination) + 38 >= FILE_IO_PATH_MAX) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary invalid owner/path"); return -1;
|
||||
}
|
||||
for (unsigned attempt = 0; attempt < 8; attempt++) {
|
||||
uint8_t random[16]; char suffix[33];
|
||||
if (random_bytes(random, sizeof(random))) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary RNG failed"); return -1; }
|
||||
for (unsigned i = 0; i < sizeof(random); i++) snprintf(suffix + i * 2, 3, "%02x", random[i]);
|
||||
snprintf(output->temporary, sizeof(output->temporary), "%s.tmp.%s", destination, suffix);
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(output->temporary, wide)) { output->temporary[0] = 0; return -1; }
|
||||
int fd = _wopen(wide, _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE);
|
||||
#else
|
||||
int fd = open(output->temporary, O_CREAT | O_EXCL | O_RDWR, 0600);
|
||||
#endif
|
||||
if (fd < 0) {
|
||||
output->temporary[0] = 0;
|
||||
if (errno == EEXIST) continue;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary open failed destination=%s errno=%d", destination, errno); return -1;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
output->file = _fdopen(fd, "wb+");
|
||||
if (!output->file) _close(fd);
|
||||
#else
|
||||
output->file = fdopen(fd, "wb+");
|
||||
if (!output->file) close(fd);
|
||||
#endif
|
||||
if (!output->file) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file fdopen failed path=%s errno=%d", output->temporary, errno); return -1; }
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file temporary opened path=%s", output->temporary);
|
||||
return 0;
|
||||
}
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary collision limit destination=%s", destination); return -1;
|
||||
}
|
||||
|
||||
int file_io_temp_finish(struct file_io_temp *output) {
|
||||
if (!output || !output->file) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file finish without open file"); return -1; }
|
||||
int error = fflush(output->file) ? (errno ? errno : EIO) : 0;
|
||||
if (fclose(output->file) && !error) error = errno ? errno : EIO;
|
||||
output->file = NULL;
|
||||
if (error) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file flush/close failed path=%s errno=%d", output->temporary, error); return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
void file_io_temp_discard(struct file_io_temp *output) {
|
||||
if (!output) return;
|
||||
if (output->file) {
|
||||
if (fclose(output->file)) DEBUG_WARN(DEBUG_CATEGORY_SYS, "file cleanup close failed errno=%d", errno);
|
||||
output->file = NULL;
|
||||
}
|
||||
if (!output->temporary[0]) return;
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
int error = file_io_wide(output->temporary, wide) ? -1 : _wremove(wide);
|
||||
#else
|
||||
int error = remove(output->temporary);
|
||||
#endif
|
||||
if (error && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_SYS, "file cleanup remove failed path=%s errno=%d", output->temporary, errno);
|
||||
output->temporary[0] = 0;
|
||||
}
|
||||
|
||||
int file_io_temp_publish(struct file_io_temp *output, const char *source, const char *destination) {
|
||||
if (!output || output->file || !output->temporary[0] || file_io_paths(source, destination)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish invalid state/paths"); return -1;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
wchar_t from[FILE_IO_PATH_MAX], to[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(output->temporary, from) || file_io_wide(destination, to)) return -1;
|
||||
if (!MoveFileExW(from, to, MOVEFILE_REPLACE_EXISTING)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish failed destination=%s winerr=%lu", destination, GetLastError()); return -1;
|
||||
}
|
||||
#else
|
||||
if (rename(output->temporary, destination)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish failed destination=%s errno=%d", destination, errno); return -1;
|
||||
}
|
||||
#endif
|
||||
output->temporary[0] = 0;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file published destination=%s", destination);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/* file_io — синхронные файловые операции для workers: UTF-8 пути, идентичность и публикация через соседний temp.
|
||||
* inspect: 0 найден, 1 отсутствует, -1 ошибка; остальные операции 0/-1, paths также 1 для одного файла.
|
||||
* Temp обнулить → open → писать в file → finish → publish; discard обязателен также при ошибке.
|
||||
* Исходник и каталоги должны оставаться стабильными на время операции. Это не защита от подмены родительских каталогов.
|
||||
* Publish заменяет имя атомарно, но не обещает сохранность при аварии питания. POSIX temp имеет права 0600. */
|
||||
#ifndef FILE_IO_H
|
||||
#define FILE_IO_H
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define FILE_IO_PATH_MAX 4096
|
||||
enum file_io_type { FILE_IO_REGULAR = 1, FILE_IO_DIRECTORY, FILE_IO_LINK, FILE_IO_OTHER };
|
||||
struct file_io_info {
|
||||
int type; // обычный файл, каталог, ссылка/reparse point или другой объект
|
||||
int64_t size; // размер в байтах, без сужения до int
|
||||
uint64_t identity[3]; // идентификатор файла внутри файловой системы, сравнивать весь массив
|
||||
};
|
||||
struct file_io_temp {
|
||||
char temporary[FILE_IO_PATH_MAX]; // только собственный temp; пусто после discard/publish
|
||||
FILE *file; // открытый wb+; NULL после finish
|
||||
};
|
||||
|
||||
// path — UTF-8; follow!=0 разрешает конечную ссылку. info обязателен, заполняется только при успехе.
|
||||
int file_io_inspect(const char *path, int follow, struct file_io_info *info);
|
||||
#ifdef _WIN32
|
||||
// Преобразовать непустой UTF-8 путь в wide[FILE_IO_PATH_MAX]; 0/-1.
|
||||
int file_io_wide(const char *path, wchar_t wide[FILE_IO_PATH_MAX]);
|
||||
#endif
|
||||
// Открыть обычный файл для чтения; путь UTF-8. NULL при ошибке, fclose принадлежит вызывающему.
|
||||
FILE *file_io_open_read(const char *path);
|
||||
// Источник должен быть regular; назначение отсутствует или regular без конечной ссылки. 1 — один файл, 0 — разные, -1 — ошибка.
|
||||
int file_io_paths(const char *source, const char *destination);
|
||||
// Создать эксклюзивный temp рядом с destination; структура заранее обнулена, даже после ошибки вызвать discard.
|
||||
int file_io_temp_open(struct file_io_temp *output, const char *destination);
|
||||
// Проверить flush/close, обнулить file при любом результате; temp остаётся собственностью output.
|
||||
int file_io_temp_finish(struct file_io_temp *output);
|
||||
// Закрыть/удалить собственный temp; повторный вызов допустим. Ошибки очистки логируются.
|
||||
void file_io_temp_discard(struct file_io_temp *output);
|
||||
// Повторно проверить paths и заменить destination закрытым temp; одинаковые файлы отклоняются.
|
||||
int file_io_temp_publish(struct file_io_temp *output, const char *source, const char *destination);
|
||||
#endif
|
||||
+6
-4
@@ -3,6 +3,8 @@
|
||||
*
|
||||
* Определяет локальный IP, который ОС выберет для отправки
|
||||
* на заданный удаленный адрес через заданный локальный адрес.
|
||||
* Передать local/remote одного семейства AF_INET или AF_INET6 и буфер result.
|
||||
* Создаёт и закрывает временный UDP-сокет; предназначен для выбора маршрута, не для обработки каждого пакета.
|
||||
*/
|
||||
|
||||
#ifndef GETMYIP_H
|
||||
@@ -26,13 +28,13 @@ extern "C" {
|
||||
* Определяет локальный IP-адрес, который ОС выберет для отправки
|
||||
* на заданный удаленный адрес через заданный локальный адрес.
|
||||
*
|
||||
* Создает временный UDP-сокет, выполняет bind на local_addr,
|
||||
* Создает временный UDP-сокет, выполняет bind на копию local_addr с портом 0,
|
||||
* connect к remote_addr и получает выбранный локальный IP через getsockname.
|
||||
*
|
||||
* @param local_addr Локальный адрес (0.0.0.0 / :: для автоопределения)
|
||||
* @param remote_addr Удаленный адрес (для определения маршрута)
|
||||
* @param local_addr Локальный адрес (0.0.0.0 / :: для автоопределения); входной порт игнорируется
|
||||
* @param remote_addr Удаленный адрес того же семейства (для определения маршрута)
|
||||
* @param result Заполняемый локальный IP (порт = 0)
|
||||
* @return 0 при успехе, -1 при ошибке
|
||||
* @return 0 при успехе, -1 при NULL, неподдерживаемом/разном семействе или ошибке socket/bind/connect/getsockname
|
||||
*/
|
||||
int get_outgoing_local_ip(const struct sockaddr_storage *local_addr,
|
||||
const struct sockaddr_storage *remote_addr,
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
# Get My IP (getmyip.h)
|
||||
|
||||
## 1. Назначение
|
||||
Определение локального IP-адреса, который ОС выберет для исходящего UDP-соединения на заданный удалённый хост. Используется в `etcp_connections.c` при установке соединения: зная remote-адрес, модуль узнаёт, какой source IP ядро назначит пакетам (нужно для проверки NAT и привязки сокета к правильному интерфейсу).
|
||||
|
||||
## 2. Как пользоваться
|
||||
Передать локальный адрес-подсказку (или `0.0.0.0` для автоопределения) и удалённый адрес. Функция создаст временный UDP-сокет, сделает bind+connect, считает выбранный IP через `getsockname` и вернёт его в `result` (порт = 0, только IP).
|
||||
|
||||
```c
|
||||
struct sockaddr_storage local_hint; // AF_INET6, sin6_addr = in6addr_any
|
||||
struct sockaddr_storage remote; // AF_INET6, адрес сервера
|
||||
struct sockaddr_storage chosen_ip;
|
||||
|
||||
// ... заполнить local_hint и remote ...
|
||||
|
||||
if (get_outgoing_local_ip(&local_hint, &remote, &chosen_ip) == 0) {
|
||||
// chosen_ip.ss_family == AF_INET6, sin6_addr содержит выбранный source IP
|
||||
// chosen_ip.sin6_port == 0
|
||||
}
|
||||
```
|
||||
|
||||
**Нюансы:**
|
||||
- Функция временно создаёт и закрывает UDP-сокет — нельзя вызывать в hot path
|
||||
- `local_addr` и `remote_addr` должны быть одного семейства (оба AF_INET или оба AF_INET6)
|
||||
- Порт в `local_addr` заменяется на 0 (чтобы не конфликтовать с основными сокетами)
|
||||
- Порт в `result` всегда 0 — только IP
|
||||
|
||||
## 3. API
|
||||
|
||||
### Функция
|
||||
- **`get_outgoing_local_ip(local_addr, remote_addr, result)`** — создаёт временный UDP-сокет, привязывает к `local_addr` (с портом 0), делает `connect` к `remote_addr` для вычисления маршрута ядром, получает выбранный source IP через `getsockname`. Возвращает 0 при успехе, -1 при ошибке (NULL-аргументы, несовпадение семейств, ошибки сокета).
|
||||
|
||||
### Зависимости
|
||||
- `socket_compat.h` — кроссплатформенные обёртки: `socket_create_udp`, `SOCKET_INVALID`, `socket_close_wrapper`, `socket_get_error`, `socket_strerror`
|
||||
+27
-10
@@ -1,4 +1,6 @@
|
||||
#include "json_flat.h"
|
||||
#include "mem.h"
|
||||
#include "debug_config.h"
|
||||
#include <string.h>
|
||||
|
||||
#define SKIP_WS(p, end) while ((p) < (end) && (*(p) == ' ' || *(p) == '\t' || *(p) == '\n' || *(p) == '\r')) (p)++
|
||||
@@ -19,6 +21,8 @@ static int json_read_string(const char** pp, const char* end, char* out, size_t
|
||||
case 'n': ch = '\n'; break;
|
||||
case 'r': ch = '\r'; break;
|
||||
case 't': ch = '\t'; break;
|
||||
case 'b': ch = '\b'; break;
|
||||
case 'f': ch = '\f'; break;
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
@@ -33,20 +37,22 @@ static int json_read_string(const char** pp, const char* end, char* out, size_t
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int json_parse_pair(const char** pp, const char* end,
|
||||
static int json_parse_pair(const char** pp, const char* end, char* scratch, size_t capacity,
|
||||
json_flat_cb cb, void* arg) {
|
||||
const char* p = *pp;
|
||||
SKIP_WS(p, end); CHECK(p, end);
|
||||
if (*p == '}') { *pp = p; return 0; }
|
||||
|
||||
char key[128], val[256];
|
||||
if (json_read_string(&p, end, key, sizeof(key)) != 0) return -1;
|
||||
char* key = scratch;
|
||||
if (json_read_string(&p, end, key, capacity) != 0) return -1;
|
||||
size_t key_size = strlen(key) + 1;
|
||||
char* val = scratch + key_size;
|
||||
SKIP_WS(p, end); CHECK(p, end);
|
||||
if (*p != ':') return -1; p++;
|
||||
SKIP_WS(p, end); CHECK(p, end);
|
||||
if (json_read_string(&p, end, val, sizeof(val)) != 0) return -1;
|
||||
if (json_read_string(&p, end, val, capacity - key_size) != 0) return -1;
|
||||
|
||||
if (cb && cb(key, val, arg) != 0) { *pp = p; return 1; }
|
||||
if (cb) { int rc = cb(key, val, arg); if (rc != 0) { *pp = p; return rc < 0 ? -1 : 1; } }
|
||||
|
||||
SKIP_WS(p, end); CHECK(p, end);
|
||||
if (*p == ',') p++;
|
||||
@@ -61,13 +67,23 @@ int json_flat_parse(const char* json, json_flat_cb cb, void* arg) {
|
||||
SKIP_WS(p, end); CHECK(p, end);
|
||||
if (*p != '{') return -1; p++;
|
||||
|
||||
/* Decoded key + value + two NULs fit within the encoded object size. */
|
||||
size_t capacity = strlen(json) + 1;
|
||||
if (capacity > UINT32_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "json_flat: input too large"); return -1; }
|
||||
char local[384];
|
||||
char* scratch = capacity <= sizeof(local) ? local : u_malloc(capacity);
|
||||
if (!scratch) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "json_flat: allocation failed bytes=%zu", capacity); return -1; }
|
||||
int result = 0;
|
||||
for (;;) {
|
||||
int rc = json_parse_pair(&p, end, cb, arg);
|
||||
if (rc < 0) return -1;
|
||||
if (rc > 0) return 0; /* callback stopped */
|
||||
SKIP_WS(p, end); CHECK(p, end);
|
||||
if (*p == '}') return 0;
|
||||
int rc = json_parse_pair(&p, end, scratch, capacity, cb, arg);
|
||||
if (rc < 0) { result = -1; break; }
|
||||
if (rc > 0) break;
|
||||
SKIP_WS(p, end);
|
||||
if (p >= end) { result = -1; break; }
|
||||
if (*p == '}') break;
|
||||
}
|
||||
if (scratch != local) u_free(scratch);
|
||||
return result;
|
||||
}
|
||||
|
||||
struct get_ctx { const char* key; char* out; size_t sz; int found; };
|
||||
@@ -81,6 +97,7 @@ static int get_cb(const char* k, const char* v, void* arg) {
|
||||
}
|
||||
|
||||
int json_flat_get(const char* json, const char* key, char* value_out, size_t value_sz) {
|
||||
if (!key || (value_out && !value_sz)) return -1;
|
||||
struct get_ctx g = { key, value_out, value_sz, 0 };
|
||||
if (json_flat_parse(json, get_cb, &g) != 0) return -1;
|
||||
return g.found ? 0 : -1;
|
||||
|
||||
+7
-1
@@ -2,7 +2,12 @@
|
||||
* @file json_flat.h
|
||||
* @brief Парсер плоского JSON без вложенности: {"key":"value",...}
|
||||
*
|
||||
* Только строковые значения. Без malloc, без зависимостей.
|
||||
* Только строковые значения и обычные escapes; \uXXXX не поддерживается.
|
||||
* Callback получает полные key/value, действительные только до его возврата.
|
||||
* parse возвращает 0 при успехе/остановке callback, -1 при ошибке; остановка не проверяет остаток ввода.
|
||||
* Синтаксис проверяется нестрого: допускаются пропущенные и завершающие запятые;
|
||||
* хвост после закрывающей } игнорируется даже без остановки callback.
|
||||
* get ищет первый ключ и копирует значение с NUL, при малом буфере обрезает; 0 найден, -1 ошибка/нет ключа.
|
||||
*/
|
||||
|
||||
#ifndef JSON_FLAT_H
|
||||
@@ -15,6 +20,7 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
typedef int (*json_flat_cb)(const char* key, const char* value, void* arg);
|
||||
/* Callback: 0 = continue, >0 = stop successfully, <0 = error. */
|
||||
|
||||
int json_flat_parse(const char* json, json_flat_cb cb, void* arg);
|
||||
int json_flat_get(const char* json, const char* key, char* value_out, size_t value_sz);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
Copyright 2001-2023 Xiph.Org, Skype Limited, Octasic,
|
||||
Jean-Marc Valin, Timothy B. Terriberry,
|
||||
CSIRO, Gregory Maxwell, Mark Borgerding,
|
||||
Erik de Castro Lopo, Mozilla, Amazon
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
- Neither the name of Internet Society, IETF or IETF Trust, nor the
|
||||
names of specific contributors, may be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
||||
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Opus is subject to the royalty-free patent licenses which are
|
||||
specified at:
|
||||
|
||||
Xiph.Org Foundation:
|
||||
https://datatracker.ietf.org/ipr/1524/
|
||||
|
||||
Microsoft Corporation:
|
||||
https://datatracker.ietf.org/ipr/1914/
|
||||
|
||||
Broadcom Corporation:
|
||||
https://datatracker.ietf.org/ipr/1526/
|
||||
@@ -1,5 +1,7 @@
|
||||
noinst_LIBRARIES = libopus_internal.a
|
||||
|
||||
EXTRA_DIST = COPYING
|
||||
|
||||
# Override global CFLAGS to disable ASAN (makes .a incompatible with PIE linking)
|
||||
CFLAGS = -g -Og -fno-omit-frame-pointer -fno-pie -fPIC
|
||||
|
||||
|
||||
+128
-43
@@ -52,15 +52,23 @@ static void on_get_trampoline(void* arg);
|
||||
static void empty_trampoline(void* arg) {
|
||||
struct ll_queue* q = (struct ll_queue*)arg;
|
||||
q->empty_call_soon_id = NULL;
|
||||
if (q->count != 0) return;
|
||||
queue_callback_fn cb = q->empty_callback;
|
||||
void* cb_arg = q->empty_callback_arg;
|
||||
q->empty_callback = NULL;
|
||||
q->empty_callback_arg = NULL;
|
||||
if (q->count == 0 && cb) cb(q, cb_arg);
|
||||
if (cb) cb(q, cb_arg);
|
||||
}
|
||||
|
||||
static void on_get_trampoline(void* arg) {
|
||||
struct ll_queue* q = (struct ll_queue*)arg;
|
||||
q->on_get_call_soon_id = NULL;
|
||||
if (!q->on_get_pending || !q->on_get) return;
|
||||
q->on_get_pending--;
|
||||
if (q->on_get_pending) {
|
||||
q->on_get_call_soon_id = uasync_call_soon(q->ua, q, on_get_trampoline);
|
||||
if (!q->on_get_call_soon_id) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] on_get scheduling failed: pending=%zu", q->name, q->on_get_pending);
|
||||
}
|
||||
if (q->on_get) q->on_get(q, q->on_get_arg);
|
||||
}
|
||||
|
||||
@@ -130,6 +138,7 @@ void queue_free(struct ll_queue* q) {
|
||||
// ВАЖНО: Не освобождаем элементы в очереди - они должны быть извлечены отдельно
|
||||
// Это упрощает архитектуру и предотвращает double-u_free
|
||||
|
||||
while (q->deferred_waiters) queue_waiter_cancel(q, q->deferred_waiters);
|
||||
// Освободить список waiters (сбросить handle->internal, отменить отложенные вызовы)
|
||||
struct queue_waiter* w = q->waiter_head;
|
||||
while (w) {
|
||||
@@ -159,6 +168,10 @@ void queue_free(struct ll_queue* q) {
|
||||
q->empty_callback_arg = NULL;
|
||||
q->on_get = NULL;
|
||||
q->on_get_arg = NULL;
|
||||
if (q->on_get_call_soon_id) uasync_call_soon_cancel(q->ua, q->on_get_call_soon_id);
|
||||
q->on_get_call_soon_id = NULL; q->on_get_pending = 0;
|
||||
if (q->head) DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] freeing nonempty queue: count=%d; entries remain caller-owned", q->name, q->count);
|
||||
for (struct ll_entry* e = q->head; e; e = e->next) e->owner = NULL;
|
||||
|
||||
u_free(q);
|
||||
}
|
||||
@@ -194,6 +207,19 @@ void queue_resume_callback(struct ll_queue* q) {
|
||||
}
|
||||
}
|
||||
|
||||
// Общий запуск коллбэка автозабора: immediate (default) или deferred через uasync_call_soon.
|
||||
// Deferred-режим очищает стек производителя перед обработкой первого элемента.
|
||||
static void queue_trigger_callback(struct ll_queue* q) {
|
||||
if (!q->head || q->callback_suspended || !q->callback) return;
|
||||
|
||||
if (q->callback_defer) {
|
||||
if (!q->resume_timeout_id)
|
||||
q->resume_timeout_id = uasync_call_soon(q->ua, q, queue_resume_timeout_cb);
|
||||
} else {
|
||||
q->callback(q, q->callback_arg);
|
||||
}
|
||||
}
|
||||
|
||||
void queue_set_size_limit(struct ll_queue* q, int lim) {
|
||||
if (!q) return;
|
||||
q->size_limit = lim;
|
||||
@@ -303,22 +329,42 @@ static uint32_t make_hash(const void* data, uint16_t len) {// алгоритм F
|
||||
return hash;
|
||||
}
|
||||
|
||||
static void waiter_defer_cb(void* arg) {
|
||||
struct queue_waiter_handle* h = (struct queue_waiter_handle*)arg;
|
||||
h->call_soon_id = NULL;
|
||||
if (h->defer_cb) h->defer_cb(h->defer_q, h->defer_arg);
|
||||
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "waiter_defer_cb: NULL callback (stale?) q=%p", (void*)h->defer_q);
|
||||
static void unlink_deferred_waiter(struct ll_queue* q, struct queue_waiter_handle* h) {
|
||||
struct queue_waiter_handle** p = &q->deferred_waiters;
|
||||
while (*p && *p != h) p = &(*p)->defer_next;
|
||||
if (*p) *p = h->defer_next;
|
||||
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] deferred waiter not found: handle=%p", q->name, h);
|
||||
h->defer_next = NULL; h->defer_q = NULL;
|
||||
}
|
||||
|
||||
static inline void queue_waiter_call(struct ll_queue* q, queue_threshold_callback_fn cb, void* arg,
|
||||
static void waiter_defer_cb(void* arg) {
|
||||
struct queue_waiter_handle* h = (struct queue_waiter_handle*)arg;
|
||||
struct ll_queue* q = h->defer_q;
|
||||
queue_threshold_callback_fn cb = h->defer_cb;
|
||||
void* cb_arg = h->defer_arg;
|
||||
h->call_soon_id = NULL;
|
||||
unlink_deferred_waiter(q, h);
|
||||
h->defer_cb = NULL; h->defer_arg = NULL;
|
||||
if (cb) cb(q, cb_arg);
|
||||
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] deferred waiter has no callback", q->name);
|
||||
}
|
||||
|
||||
static inline int queue_waiter_call(struct ll_queue* q, queue_threshold_callback_fn cb, void* arg,
|
||||
struct queue_waiter_handle* h) {
|
||||
if (q->waiter_defer) {
|
||||
if (h->call_soon_id) { DEBUG_WARN(DEBUG_CATEGORY_SYS, "queue_waiter_call: overwriting existing call_soon_id=%p", h->call_soon_id); uasync_call_soon_cancel(q->ua, h->call_soon_id); }
|
||||
if (h->call_soon_id) queue_waiter_cancel(h->defer_q, h);
|
||||
h->defer_q = q; h->defer_cb = cb; h->defer_arg = arg;
|
||||
h->call_soon_id = uasync_call_soon(q->ua, h, waiter_defer_cb);
|
||||
if (h->call_soon_id) { h->defer_next = q->deferred_waiters; q->deferred_waiters = h; }
|
||||
else {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] deferred waiter scheduling failed: handle=%p", q->name, h);
|
||||
h->defer_q = NULL; h->defer_cb = NULL; h->defer_arg = NULL;
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
cb(q, arg);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Проверить и запустить первый ожидающий коллбэк (FIFO / round-robin)
|
||||
@@ -338,13 +384,18 @@ static void check_waiters(struct ll_queue* q) {
|
||||
|
||||
queue_threshold_callback_fn cb = waiter->callback;
|
||||
void* arg = waiter->callback_arg;
|
||||
u_free(waiter);
|
||||
|
||||
if (cb) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "check_waiters: waking head waiter, count=%d<=%d, bytes=%zu<=%zu",
|
||||
q->count, q->threshold_max_packets, q->total_bytes, q->threshold_max_bytes);
|
||||
queue_waiter_call(q, cb, arg, h);
|
||||
if (queue_waiter_call(q, cb, arg, h) < 0) {
|
||||
// Отказ планирования не теряет ожидание: следующий get/remove повторит попытку.
|
||||
waiter->next = q->waiter_head; q->waiter_head = waiter;
|
||||
if (!q->waiter_tail) q->waiter_tail = waiter;
|
||||
h->internal = waiter;
|
||||
return;
|
||||
}
|
||||
}
|
||||
u_free(waiter);
|
||||
}
|
||||
|
||||
int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
|
||||
@@ -353,7 +404,7 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
|
||||
queue_check_thread(q);
|
||||
#endif
|
||||
|
||||
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
|
||||
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put detected! entry=%p prev=%p next=%p len=%u",
|
||||
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next, entry->len);
|
||||
abort();
|
||||
@@ -367,6 +418,7 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
|
||||
}
|
||||
|
||||
if (q->size_limit >= 0 && q->count >= q->size_limit) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
|
||||
queue_dgram_free(entry);
|
||||
queue_entry_free(entry);
|
||||
return -1;
|
||||
@@ -378,19 +430,18 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
|
||||
if (q->tail) q->tail->next = entry; else q->head = entry;
|
||||
q->tail = entry;
|
||||
|
||||
entry->owner = q;
|
||||
q->count++;
|
||||
entry->int_len = entry->len;
|
||||
q->total_bytes += entry->int_len;
|
||||
|
||||
// НЕ вызываем add_to_hash
|
||||
|
||||
if (q->count == 1 && !q->callback_suspended && q->callback) {
|
||||
q->callback(q, q->callback_arg);
|
||||
}
|
||||
|
||||
#ifdef QUEUE_DEBUG
|
||||
queue_check_consistency(q);
|
||||
#endif
|
||||
// Пользовательский callback может освободить очередь.
|
||||
if (q->count == 1) queue_trigger_callback(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -400,7 +451,7 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) {
|
||||
queue_check_thread(q);
|
||||
#endif
|
||||
|
||||
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
|
||||
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put_with_index detected! entry=%p prev=%p next=%p",
|
||||
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next);
|
||||
abort();
|
||||
@@ -416,6 +467,7 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) {
|
||||
entry->index_hash = make_hash(entry->data + q->index_offset, q->index_size);
|
||||
|
||||
if (q->size_limit >= 0 && q->count >= q->size_limit) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
|
||||
queue_dgram_free(entry);
|
||||
queue_entry_free(entry);
|
||||
return -1;
|
||||
@@ -427,19 +479,18 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) {
|
||||
if (q->tail) q->tail->next = entry; else q->head = entry;
|
||||
q->tail = entry;
|
||||
|
||||
entry->owner = q;
|
||||
q->count++;
|
||||
entry->int_len = entry->len;
|
||||
q->total_bytes += entry->int_len;
|
||||
|
||||
add_to_hash(q, entry);
|
||||
|
||||
if (q->count == 1 && !q->callback_suspended && q->callback) {
|
||||
q->callback(q, q->callback_arg);
|
||||
}
|
||||
|
||||
#ifdef QUEUE_DEBUG
|
||||
queue_check_consistency(q);
|
||||
#endif
|
||||
// Пользовательский callback может освободить очередь.
|
||||
if (q->count == 1) queue_trigger_callback(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -449,7 +500,7 @@ int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry) {
|
||||
queue_check_thread(q);
|
||||
#endif
|
||||
|
||||
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
|
||||
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put_first detected! entry=%p prev=%p next=%p",
|
||||
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next);
|
||||
abort();
|
||||
@@ -462,22 +513,25 @@ int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (q->size_limit >= 0 && q->count >= q->size_limit) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
|
||||
queue_dgram_free(entry); queue_entry_free(entry); return -1;
|
||||
}
|
||||
entry->next = q->head;
|
||||
entry->prev = NULL;
|
||||
if (q->head) q->head->prev = entry; else q->tail = entry;
|
||||
q->head = entry;
|
||||
|
||||
entry->owner = q;
|
||||
q->count++;
|
||||
entry->int_len = entry->len;
|
||||
q->total_bytes += entry->int_len;
|
||||
|
||||
if (q->count == 1 && !q->callback_suspended && q->callback) {
|
||||
q->callback(q, q->callback_arg);
|
||||
}
|
||||
|
||||
#ifdef QUEUE_DEBUG
|
||||
queue_check_consistency(q);
|
||||
#endif
|
||||
// Пользовательский callback может освободить очередь.
|
||||
if (q->count == 1) queue_trigger_callback(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -488,7 +542,7 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry)
|
||||
queue_check_thread(q);
|
||||
#endif
|
||||
|
||||
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
|
||||
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put_first_with_index detected! entry=%p prev=%p next=%p",
|
||||
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next);
|
||||
abort();
|
||||
@@ -504,6 +558,7 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry)
|
||||
entry->index_hash = make_hash(entry->data + q->index_offset, q->index_size);
|
||||
|
||||
if (q->size_limit >= 0 && q->count >= q->size_limit) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
|
||||
queue_dgram_free(entry);
|
||||
queue_entry_free(entry);
|
||||
return -1;
|
||||
@@ -514,19 +569,18 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry)
|
||||
if (q->head) q->head->prev = entry; else q->tail = entry;
|
||||
q->head = entry;
|
||||
|
||||
entry->owner = q;
|
||||
q->count++;
|
||||
entry->int_len = entry->len;
|
||||
q->total_bytes += entry->int_len;
|
||||
|
||||
add_to_hash(q, entry);
|
||||
|
||||
if (q->count == 1 && !q->callback_suspended && q->callback) {
|
||||
q->callback(q, q->callback_arg);
|
||||
}
|
||||
|
||||
#ifdef QUEUE_DEBUG
|
||||
queue_check_consistency(q);
|
||||
#endif
|
||||
// Пользовательский callback может освободить очередь.
|
||||
if (q->count == 1) queue_trigger_callback(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -549,6 +603,7 @@ struct ll_entry* queue_data_get(struct ll_queue* q) {
|
||||
|
||||
entry->next = NULL;
|
||||
entry->prev = NULL;
|
||||
entry->owner = NULL;
|
||||
|
||||
remove_from_hash(q, entry);
|
||||
|
||||
@@ -557,18 +612,21 @@ struct ll_entry* queue_data_get(struct ll_queue* q) {
|
||||
// Приостановить коллбэки для предотвращения рекурсии
|
||||
q->callback_suspended = 1;
|
||||
|
||||
// Проверить ожидающие коллбэки
|
||||
check_waiters(q);
|
||||
|
||||
if (q->count == 0 && q->empty_callback && !q->empty_call_soon_id)
|
||||
q->empty_call_soon_id = uasync_call_soon(q->ua, q, (timeout_callback_t)empty_trampoline);
|
||||
|
||||
if (q->on_get)
|
||||
uasync_call_soon(q->ua, q, (timeout_callback_t)on_get_trampoline);
|
||||
if (q->on_get) {
|
||||
q->on_get_pending++;
|
||||
if (!q->on_get_call_soon_id) {
|
||||
q->on_get_call_soon_id = uasync_call_soon(q->ua, q, on_get_trampoline);
|
||||
if (!q->on_get_call_soon_id) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] on_get scheduling failed: pending=%zu", q->name, q->on_get_pending);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef QUEUE_DEBUG
|
||||
queue_check_consistency(q);// !!!! for debug
|
||||
#endif
|
||||
check_waiters(q);
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -647,6 +705,11 @@ void queue_set_waiter_defer(struct ll_queue* q, int enable) {
|
||||
q->waiter_defer = enable;
|
||||
}
|
||||
|
||||
void queue_set_callback_defer(struct ll_queue* q, int enable) {
|
||||
if (!q) return;
|
||||
q->callback_defer = enable;
|
||||
}
|
||||
|
||||
void queue_set_empty_callback(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg) {
|
||||
if (!q) return;
|
||||
void* old = q->empty_call_soon_id;
|
||||
@@ -662,18 +725,24 @@ void queue_set_on_get(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg) {
|
||||
if (!q) return;
|
||||
q->on_get = cbk_fn;
|
||||
q->on_get_arg = arg;
|
||||
if (!cbk_fn) {
|
||||
if (q->on_get_call_soon_id) uasync_call_soon_cancel(q->ua, q->on_get_call_soon_id);
|
||||
q->on_get_call_soon_id = NULL; q->on_get_pending = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
|
||||
queue_threshold_callback_fn callback, void* arg) {
|
||||
if (!q || !h) return -1;
|
||||
if (!q || !h || !callback) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "queue_waiter_wait: invalid arguments q=%p handle=%p callback=%p", q, h, (void*)callback);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (h->internal) queue_waiter_cancel(q, h);
|
||||
if (h->internal || h->call_soon_id) queue_waiter_cancel(h->call_soon_id ? h->defer_q : q, h);
|
||||
|
||||
if (q->count <= q->threshold_max_packets
|
||||
&& (q->threshold_max_bytes == 0 || q->total_bytes <= q->threshold_max_bytes)) {
|
||||
queue_waiter_call(q, callback, arg, h);
|
||||
return 1;
|
||||
return queue_waiter_call(q, callback, arg, h) < 0 ? -1 : 1;
|
||||
}
|
||||
|
||||
struct queue_waiter* waiter = u_malloc(sizeof(struct queue_waiter));
|
||||
@@ -694,9 +763,18 @@ int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
|
||||
}
|
||||
|
||||
void queue_waiter_cancel(struct ll_queue* q, struct queue_waiter_handle* h) {
|
||||
if (!q || !h || !h->internal) return;
|
||||
if (h->call_soon_id) { uasync_call_soon_cancel(q->ua, h->call_soon_id); h->call_soon_id = NULL; }
|
||||
|
||||
if (!q || !h) return;
|
||||
/* Отложенный вызов (waiter_defer) может висеть и при internal==NULL:
|
||||
* queue_waiter_wait по ветке «очередь свободна» планирует call_soon_id,
|
||||
* не регистрируя waiter. Ранний выход по !internal оставлял бы висячий
|
||||
* callback с arg=handle, который после teardown разыменовывал бы freed-память. */
|
||||
if (h->call_soon_id) {
|
||||
struct ll_queue* deferred_q = h->defer_q;
|
||||
uasync_call_soon_cancel(deferred_q->ua, h->call_soon_id); h->call_soon_id = NULL;
|
||||
unlink_deferred_waiter(deferred_q, h); h->defer_cb = NULL; h->defer_arg = NULL;
|
||||
}
|
||||
if (!h->internal) return;
|
||||
|
||||
struct queue_waiter* waiter = h->internal;
|
||||
struct queue_waiter* prev = NULL;
|
||||
struct queue_waiter* curr = q->waiter_head;
|
||||
@@ -754,6 +832,10 @@ struct ll_entry* queue_find_next_by_index(struct ll_queue* q, const void* index_
|
||||
|
||||
int queue_remove_data(struct ll_queue* q, struct ll_entry* entry) {
|
||||
if (!q || !entry) return -1;
|
||||
if (entry->owner != q) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] cannot remove foreign entry=%p owner=%p", q->name, entry, entry->owner);
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef QUEUE_THREAD_CHECK
|
||||
queue_check_thread(q);
|
||||
@@ -777,6 +859,7 @@ int queue_remove_data(struct ll_queue* q, struct ll_entry* entry) {
|
||||
|
||||
entry->next = NULL;
|
||||
entry->prev = NULL;
|
||||
entry->owner = NULL;
|
||||
|
||||
remove_from_hash(q, entry);
|
||||
|
||||
@@ -784,6 +867,8 @@ int queue_remove_data(struct ll_queue* q, struct ll_entry* entry) {
|
||||
#ifdef QUEUE_DEBUG
|
||||
queue_check_consistency(q);// !!!! for debug
|
||||
#endif
|
||||
if (q->count == 0 && q->empty_callback && !q->empty_call_soon_id)
|
||||
q->empty_call_soon_id = uasync_call_soon(q->ua, q, empty_trampoline);
|
||||
check_waiters(q);
|
||||
return 0;
|
||||
}
|
||||
+114
-52
@@ -1,6 +1,10 @@
|
||||
#ifndef LL_QUEUE_H
|
||||
#define LL_QUEUE_H
|
||||
|
||||
/* Очередь одного uasync-потока; наличие pthread-заголовков не делает её межпоточной.
|
||||
* put может синхронно вызвать consumer/waiter. Держать владельцев живыми до завершения callbacks,
|
||||
* waiter handle обнулять целиком и отменять до освобождения; entry и dgram освобождаются отдельно. */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -37,8 +41,8 @@ extern "C" {
|
||||
* @note Важные правила использования автозабора (queue_set_callback):
|
||||
* 1. Коллбэк получает управление только если в очереди есть элементы И !callback_suspended
|
||||
* 2. Внутри коллбэка ОБЯЗАТЕЛЬНО: queue_data_get() → обработать элемент → queue_resume_callback()
|
||||
* 3. Без вызова queue_resume_callback очередь навсегда застрянет
|
||||
* 4. Не извлекайте элементы вручную вне коллбэка — это нарушит логику
|
||||
* 3. Без queue_resume_callback автозабор остаётся приостановленным
|
||||
* 4. Ручное извлечение допустимо; для продолжения автозабора после него тоже нужен resume
|
||||
* 5. queue_data_get() автоматически suspend'ит коллбэки для предотвращения рекурсии
|
||||
*
|
||||
* @note Память:
|
||||
@@ -57,10 +61,11 @@ struct queue_waiter_handle;
|
||||
* @brief Элемент очереди (переменного размера).
|
||||
*
|
||||
* Память выделяется одним блоком: [struct ll_entry + data[size]].
|
||||
* Поле dgram — отдельный блок (malloc / пул / внешняя память).
|
||||
* Поле dgram — отдельный блок (u_malloc / пул / внешняя память со своим dgram_free_fn).
|
||||
*/
|
||||
struct ll_entry {
|
||||
char* name;
|
||||
struct ll_queue* owner; // Владелец элемента; NULL после извлечения
|
||||
struct ll_entry* next; // Следующий элемент в очереди
|
||||
struct ll_entry* prev; // Предыдущий элемент в очереди
|
||||
uint16_t size; // Размер пользовательского буфера data[] (байт)
|
||||
@@ -70,9 +75,9 @@ struct ll_entry {
|
||||
uint8_t* dgram; // Указатель на данные пакета
|
||||
void (*dgram_free_fn)(uint8_t* data); // Кастомная функция освобождения dgram
|
||||
struct memory_pool* dgram_pool; // Пул для dgram (если используется)
|
||||
uint32_t index_hash; // Хеш по первым 4 байтам индекса
|
||||
uint32_t index_hash; // FNV-1a (32 бита) по всем index_size байтам data[] от index_offset
|
||||
struct ll_entry* hash_next; // Следующий в хеш-цепочке
|
||||
struct memory_pool* pool; // Пул, из которого выделен сам entry (NULL = malloc)
|
||||
struct memory_pool* pool; // Пул, из которого выделен сам entry (NULL = u_malloc)
|
||||
uint8_t data[0]; // Гибкий массив пользовательских данных (размер = size)
|
||||
};
|
||||
|
||||
@@ -123,6 +128,7 @@ struct queue_waiter_handle {
|
||||
queue_threshold_callback_fn defer_cb;
|
||||
void* defer_arg;
|
||||
void* call_soon_id; ///< хендл отложенного вызова (для отмены)
|
||||
struct queue_waiter_handle* defer_next; ///< список отложенных уведомлений очереди
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -146,9 +152,11 @@ struct ll_queue {
|
||||
|
||||
struct queue_waiter* waiter_head; // Голова списка ожидающих
|
||||
struct queue_waiter* waiter_tail; // Хвост списка ожидающих
|
||||
struct queue_waiter_handle* deferred_waiters; // Уже запланированные уведомления
|
||||
int threshold_max_packets; // Общий порог: макс. кол-во элементов (по умолчанию 0)
|
||||
size_t threshold_max_bytes; // Общий порог: макс. объём данных (0 = не проверять)
|
||||
int waiter_defer; // 0=immediate callback, 1=defer via uasync_call_soon
|
||||
int callback_defer; // 0=immediate callback из put (default), 1=defer via uasync_call_soon (очистка стека)
|
||||
|
||||
queue_callback_fn empty_callback; // одноразовый коллбэк при опустошении очереди
|
||||
void* empty_callback_arg;
|
||||
@@ -156,6 +164,8 @@ struct ll_queue {
|
||||
|
||||
queue_callback_fn on_get; // коллбэк после каждого queue_data_get (deferred)
|
||||
void* on_get_arg;
|
||||
void* on_get_call_soon_id;
|
||||
size_t on_get_pending;
|
||||
|
||||
struct ll_entry** hash_table; // Хеш-таблица для поиска по id (если hash_size > 0)
|
||||
size_t hash_size; // Размер хеш-таблицы
|
||||
@@ -171,7 +181,7 @@ struct ll_queue {
|
||||
#endif
|
||||
};
|
||||
|
||||
/* ==================== Создание / уничтожение ==================== */
|
||||
// ==================== Создание / уничтожение ====================
|
||||
|
||||
/**
|
||||
* @brief Создаёт новую пустую очередь.
|
||||
@@ -179,8 +189,8 @@ struct ll_queue {
|
||||
* @param hash_size размер хеш-таблицы (0 = поиск по id отключён)
|
||||
* @param index_offset смещение ключа индекса в data[] каждого entry (0 если hash_size=0)
|
||||
* @param index_size размер ключа индекса (0 если hash_size=0)
|
||||
* @param name имя очереди для отладки
|
||||
* @return указатель на очередь или NULL при ошибке выделения памяти
|
||||
* @param name заимствованное имя для отладки, должно жить до queue_free
|
||||
* @return указатель на очередь или NULL при NULL ua / ошибке выделения памяти
|
||||
*/
|
||||
struct ll_queue* queue_new(struct UASYNC* ua, size_t hash_size, uint16_t index_offset, uint16_t index_size, char* name);
|
||||
|
||||
@@ -190,42 +200,49 @@ struct ll_queue* queue_new(struct UASYNC* ua, size_t hash_size, uint16_t index_o
|
||||
*
|
||||
* @warning НЕ ОСВОБОЖДАЕТ элементы в очереди!
|
||||
* Их нужно извлечь через queue_data_get() и освободить через queue_entry_free().
|
||||
* Перед дренажем снять callback и отменить ожидания владельцев, затем для каждого entry освободить dgram и entry.
|
||||
* queue_free отменяет свои отложенные уведомления и waiters; user_arg не освобождает. NULL допустим.
|
||||
*/
|
||||
void queue_free(struct ll_queue* q);
|
||||
|
||||
/* ==================== Конфигурация ==================== */
|
||||
// ==================== Конфигурация ====================
|
||||
|
||||
/**
|
||||
* @brief Устанавливает максимальное количество элементов в очереди.
|
||||
* @param q очередь
|
||||
* @param lim лимит (-1 = без ограничения)
|
||||
* @param lim лимит (любое отрицательное значение = без ограничения)
|
||||
*
|
||||
* При превышении лимита новые элементы автоматически освобождаются.
|
||||
*/
|
||||
void queue_set_size_limit(struct ll_queue* q, int lim);
|
||||
|
||||
/* ==================== Автозабор элементов ==================== */
|
||||
// ==================== Автозабор элементов ====================
|
||||
|
||||
/**
|
||||
* @brief Устанавливает коллбэк для автоматического извлечения элементов.
|
||||
* @param q очередь
|
||||
* @param cbk_fn функция-коллбэк
|
||||
* @param cbk_fn функция-коллбэк (NULL отключает автозабор)
|
||||
* @param arg пользовательский аргумент
|
||||
*
|
||||
* Коллбэк вызывается автоматически при наличии элементов и разрешённом состоянии.
|
||||
* Только заменяет функцию и аргумент: не запускает обработку, не снимает callback_suspended
|
||||
* и не отменяет уже запланированный resume. Отложенный resume использует новые функцию и аргумент.
|
||||
* Для запуска автозабора из уже заполненной очереди вызовите queue_resume_callback().
|
||||
*/
|
||||
void queue_set_callback(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg);
|
||||
|
||||
/**
|
||||
* @brief Возобновляет работу коллбэка после обработки элемента в следующем event loop.
|
||||
* @brief Разрешает автозабор и планирует отложенный вызов коллбэка.
|
||||
* @param q очередь
|
||||
*
|
||||
* @warning ОБЯЗАТЕЛЬНО вызывать после завершения обработки элемента в коллбэке.
|
||||
* Иначе автозабор элементов навсегда остановится.
|
||||
* Сразу снимает callback_suspended. Если есть элементы и callback, планирует uasync_call_soon,
|
||||
* если resume ещё не запланирован. При исполнении повторно проверяет наличие элементов,
|
||||
* callback и отсутствие приостановки. Последующий put в пустую очередь может вызвать callback
|
||||
* синхронно, если callback_defer == 0.
|
||||
* @note Вызывать после завершения обработки извлечённого элемента для продолжения автозабора.
|
||||
*/
|
||||
void queue_resume_callback(struct ll_queue* q);
|
||||
|
||||
/* ==================== Пороговое ожидание (backpressure) ==================== */
|
||||
// ==================== Пороговое ожидание (backpressure) ====================
|
||||
|
||||
/**
|
||||
* @brief Устанавливает общий порог освобождения очереди.
|
||||
@@ -234,87 +251,128 @@ void queue_resume_callback(struct ll_queue* q);
|
||||
* @param max_bytes максимальный объём данных (0 = не проверять)
|
||||
*
|
||||
* Порог — общий на всю очередь. Все waiters в связном списке используют этот порог.
|
||||
* При достижении порога вызывается следующий waiter из списка (FIFO / round-robin).
|
||||
* После успешного get/remove при выполнении порога вызывается один следующий waiter из FIFO-списка.
|
||||
* Сам setter старые ожидания не проверяет. Байт-порог использует int_len, зафиксированный при put.
|
||||
* По умолчанию: max_packets=0, max_bytes=0 (пустая очередь).
|
||||
*/
|
||||
void queue_set_threshold(struct ll_queue* q, int max_packets, size_t max_bytes);
|
||||
|
||||
// enable=0 — синхронный waiter (по умолчанию), !=0 — через uasync_call_soon.
|
||||
// Deferred-уведомление не резервирует место и повторно порог не проверяет: очередь к исполнению может заполниться.
|
||||
// Повторную регистрацию producer из его callback при свободной очереди откладывать, чтобы избежать рекурсии.
|
||||
void queue_set_waiter_defer(struct ll_queue* q, int enable);
|
||||
|
||||
/**
|
||||
* @brief Управляет способом вызова коллбэка автозабора при добавлении элемента.
|
||||
* @param q очередь
|
||||
* @param enable 0 — первый запуск коллбэка из queue_data_put* вызывается синхронно (по умолчанию);
|
||||
* 1 — первый запуск откладывается через uasync_call_soon (очищает стек вызывающего).
|
||||
*
|
||||
* @note Влияет только на первый запуск из put. Повторные запуски (queue_resume_callback)
|
||||
* всегда идут через uasync_call_soon.
|
||||
*/
|
||||
void queue_set_callback_defer(struct ll_queue* q, int enable);
|
||||
|
||||
/**
|
||||
* @brief Устанавливает одноразовый коллбэк при опустошении очереди (count==0).
|
||||
* @param q очередь
|
||||
* @param cbk_fn коллбэк (NULL для отмены)
|
||||
* @param arg пользовательский аргумент
|
||||
*
|
||||
* Коллбэк вызывается асинхронно (через uasync_call_soon) когда count становится 0.
|
||||
* После вызова сбрасывается — для повторного использования нужно вызвать снова.
|
||||
* Заменяет регистрацию и отменяет её прежний отложенный вызов. При count == 0 сразу планирует
|
||||
* uasync_call_soon; иначе вызов планируется при опустошении через queue_data_get/queue_remove_data.
|
||||
* При исполнении снова проверяет count: если очередь заполнилась, регистрация сохраняется
|
||||
* до следующего опустошения. Функция и аргумент сбрасываются ДО вызова callback,
|
||||
* поэтому внутри него можно зарегистрировать новое уведомление.
|
||||
*/
|
||||
void queue_set_empty_callback(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg);
|
||||
|
||||
/**
|
||||
* @brief Устанавливает коллбэк вызываемый после каждого queue_data_get (deferred через call_soon).
|
||||
* @brief Устанавливает коллбэк после каждого успешного queue_data_get (deferred через call_soon).
|
||||
* @param q очередь
|
||||
* @param cbk_fn коллбэк (NULL для отмены)
|
||||
* @param arg пользовательский аргумент
|
||||
*
|
||||
* Уведомления подсчитываются, не сливаются в одно; remove их не создаёт.
|
||||
* NULL отменяет отложенный вызов и обнуляет накопленные уведомления. Замена использует новые функцию/arg.
|
||||
*/
|
||||
void queue_set_on_get(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg);
|
||||
|
||||
/**
|
||||
* @brief Регистрирует ожидание освобождения очереди до общего порога.
|
||||
* @param q очередь
|
||||
* @param h указатель на handle (создаётся как `struct queue_waiter_handle h = {0}` или `h.internal = NULL`)
|
||||
* @param h handle, полностью обнулённый перед первым использованием: `struct queue_waiter_handle h = {0}`
|
||||
* @param callback функция, вызываемая при достижении порога
|
||||
* @param arg аргумент коллбэка
|
||||
* @return 1 — условие уже выполнено (callback вызван немедленно), 0 — зарегистрирован в списке, -1 — ошибка
|
||||
* @return 1 — условие выполнено (callback вызван или отложен), 0 — ожидает порог, -1 — ошибка
|
||||
*
|
||||
* Если handle уже ожидает (h->internal != NULL) — старый waiter отменяется и создаётся новый.
|
||||
* При корректных аргументах повторная регистрация в той же очереди отменяет прежнее ожидание.
|
||||
* Если h->internal != NULL, перед переносом в другую очередь обязательно вызовите
|
||||
* queue_waiter_cancel(old_q, h): очередь старого ожидания в handle не хранится.
|
||||
* Уже отложенный callback (h->call_soon_id != NULL) отменяется по сохранённому h->defer_q,
|
||||
* поэтому в этом состоянии handle можно передать другой очереди без отдельной отмены.
|
||||
* При queue_free отменяются также уже запланированные уведомления.
|
||||
*/
|
||||
int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
|
||||
queue_threshold_callback_fn callback, void* arg);
|
||||
|
||||
/**
|
||||
* @brief Отменяет ожидание (удаляет из списка, если зарегистрирован).
|
||||
* @param q очередь
|
||||
* @param q очередь исходного ожидания (для h->internal); не NULL
|
||||
* @param h указатель на handle
|
||||
*
|
||||
* Безопасно вызывать в любом состоянии: если waiter не зарегистрирован — ничего не делает.
|
||||
* Отложенный callback отменяется через сохранённую h->defer_q. Если ожидания и отложенного
|
||||
* callback нет — ничего не делает. При h->internal != NULL передавать другую очередь нельзя.
|
||||
*/
|
||||
void queue_waiter_cancel(struct ll_queue* q, struct queue_waiter_handle* h);
|
||||
|
||||
/* ==================== Работа с данными ==================== */
|
||||
// ==================== Работа с данными ====================
|
||||
|
||||
/**
|
||||
* Общие правила владения для всех четырёх queue_data_put*:
|
||||
* - При q == NULL или entry == NULL возвращается -1 без освобождения памяти.
|
||||
* - Элемент должен быть извлечён из прежней очереди: owner, prev и next должны быть NULL.
|
||||
* Повторная вставка связанного элемента приводит к abort().
|
||||
* - При остальных отказах (лимит, неверный вид put или границы индекса) освобождаются
|
||||
* сначала dgram через queue_dgram_free(), затем entry через queue_entry_free().
|
||||
* - При успехе владение передаётся очереди/потребителю. Put может синхронно вызвать callback,
|
||||
* который обработает и освободит entry и даже очередь до возврата из put.
|
||||
* Использовать эти указатели после put можно только при гарантированном времени жизни.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Добавляет элемент в конец очереди (FIFO).
|
||||
* @param q очередь
|
||||
* @param entry элемент
|
||||
* @param id идентификатор для поиска
|
||||
* @return 0 — успех, -1 — превышен лимит (элемент освобождён)
|
||||
* @param q очередь без хеш-таблицы (hash_size == 0)
|
||||
* @param entry элемент; правила передачи владения см. выше
|
||||
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или hash_size > 0
|
||||
*/
|
||||
int queue_data_put(struct ll_queue* q, struct ll_entry* entry);
|
||||
|
||||
/**
|
||||
* @brief Добавляет элемент в начало очереди (LIFO, высокий приоритет).
|
||||
* @param q очередь
|
||||
* @param entry элемент
|
||||
* @param id идентификатор
|
||||
* @return 0 — успех, -1 — превышен лимит (элемент освобождён)
|
||||
* @param q очередь без хеш-таблицы (hash_size == 0)
|
||||
* @param entry элемент; правила передачи владения см. выше
|
||||
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или hash_size > 0
|
||||
*/
|
||||
int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry);
|
||||
|
||||
/**
|
||||
* @brief Добавляет элемент в конец очереди с хеш-индексом (использует index_offset/index_size из очереди).
|
||||
* @param q очередь
|
||||
* @param entry элемент
|
||||
* @return 0 — успех, -1 — ошибка
|
||||
* @param q очередь с настроенной хеш-таблицей (hash_size > 0, index_size > 0)
|
||||
* @param entry элемент; ключ занимает index_size байт в data[] от index_offset и должен помещаться в entry->size
|
||||
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или неверные границы/нулевой размер индекса
|
||||
* @note Владение см. выше. При hash_size == 0 реализация может вернуть 0, добавив элемент без индексации;
|
||||
* наличие хеш-таблицы — предусловие, а не гарантированная проверка с возвратом -1.
|
||||
*/
|
||||
int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry);
|
||||
|
||||
/**
|
||||
* @brief Добавляет элемент в начало очереди с хеш-индексом (использует index_offset/index_size из очереди).
|
||||
* @param q очередь
|
||||
* @param entry элемент
|
||||
* @return 0 — успех, -1 — ошибка
|
||||
* @param q очередь с настроенной хеш-таблицей (hash_size > 0, index_size > 0)
|
||||
* @param entry элемент; ключ занимает index_size байт в data[] от index_offset и должен помещаться в entry->size
|
||||
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или неверные границы/нулевой размер индекса
|
||||
* @note Владение см. выше. При hash_size == 0 реализация может вернуть 0, добавив элемент без индексации;
|
||||
* наличие хеш-таблицы — предусловие, а не гарантированная проверка с возвратом -1.
|
||||
*/
|
||||
int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry);
|
||||
|
||||
@@ -336,35 +394,35 @@ struct ll_entry* queue_data_get(struct ll_queue* q);
|
||||
*/
|
||||
int queue_entry_count(struct ll_queue* q);
|
||||
|
||||
/* ==================== Управление памятью элементов ==================== */
|
||||
// ==================== Управление памятью элементов ====================
|
||||
|
||||
// проверить правильность очереди (корректность цепочки, счетчики элементов и байтов)
|
||||
int queue_check_consistency(struct ll_queue* q);
|
||||
|
||||
/**
|
||||
* @brief Выделяет элемент с отдельным буфером dgram (malloc).
|
||||
* @brief Выделяет элемент с отдельным буфером dgram через u_malloc (entry->len=0, memlen=len).
|
||||
* @param len требуемый размер dgram
|
||||
* @return элемент или NULL при ошибке
|
||||
*/
|
||||
struct ll_entry* ll_alloc_lldgram(uint16_t len);
|
||||
|
||||
/**
|
||||
* @brief Создаёт элемент с пользовательским буфером data[] (malloc одним блоком).
|
||||
* @param data_size размер data[]
|
||||
* @brief Создаёт обнулённый элемент с пользовательским буфером data[] (u_malloc одним блоком).
|
||||
* @param data_size размер data[], должен помещаться в uint16_t
|
||||
* @return элемент или NULL при ошибке
|
||||
*/
|
||||
struct ll_entry* queue_entry_new(size_t data_size);
|
||||
|
||||
/**
|
||||
* @brief Создаёт элемент из пула памяти.
|
||||
* @param pool пул
|
||||
* @return элемент или NULL, если пул пуст
|
||||
* @param pool пул с object_size >= sizeof(struct ll_entry); разность размеров должна помещаться в uint16_t
|
||||
* @return элемент или NULL при ошибке выделения; пустой кеш пула пополняется автоматически
|
||||
*/
|
||||
struct ll_entry* queue_entry_new_from_pool(struct memory_pool* pool);
|
||||
|
||||
/**
|
||||
* @brief Освобождает структуру элемента (возвращает в пул или free).
|
||||
* @param entry элемент
|
||||
* @brief Освобождает структуру элемента (возвращает в пул или u_free).
|
||||
* @param entry извлечённый из очереди элемент; NULL допустим
|
||||
*
|
||||
* @note НЕ освобождает dgram! Используйте queue_dgram_free() отдельно.
|
||||
*/
|
||||
@@ -373,10 +431,14 @@ void queue_entry_free(struct ll_entry* entry);
|
||||
/**
|
||||
* @brief Освобождает буфер dgram элемента.
|
||||
* @param entry элемент
|
||||
*
|
||||
* При dgram != NULL вызывает dgram_free_fn, если она задана; иначе возвращает буфер в dgram_pool,
|
||||
* если задан пул; иначе вызывает u_free. Затем сбрасывает dgram и len. Сам entry не освобождает.
|
||||
* Внешнему буферу нужен подходящий dgram_free_fn.
|
||||
*/
|
||||
void queue_dgram_free(struct ll_entry* entry);
|
||||
|
||||
/* ==================== Поиск и удаление ==================== */
|
||||
// ==================== Поиск и удаление ====================
|
||||
|
||||
/**
|
||||
* @brief Находит элемент по ключу индекса (требуется hash_size > 0 при создании).
|
||||
@@ -406,12 +468,12 @@ struct ll_entry* queue_find_next_by_index(struct ll_queue* q,
|
||||
*/
|
||||
int queue_remove_data(struct ll_queue* q, struct ll_entry* entry);
|
||||
|
||||
/* ==================== Утилиты ==================== */
|
||||
// ==================== Утилиты ====================
|
||||
|
||||
/**
|
||||
* @brief Возвращает суммарный объём данных в очереди.
|
||||
* @param q очередь
|
||||
* @return количество байт
|
||||
* @return сумма int_len (снимки len при put); изменение len связанного entry счётчик не пересчитывает
|
||||
*/
|
||||
static inline size_t queue_total_bytes(struct ll_queue* q) {
|
||||
return q->total_bytes;
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
# ll_queue — Двусвязная FIFO-очередь с автозабором и backpressure
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
Двусвязная очередь элементов (FIFO) с автоматическим вызовом callback при появлении данных, пороговым ожиданием освобождения (backpressure), хеш-индексом для поиска и поддержкой пулов памяти. Используется как основной механизм передачи данных между компонентами внутри одного потока uasyc: TUN ↔ ETCP ↔ routing ↔ normalizer и т.д.
|
||||
|
||||
Работает **строго в одном потоке** (один uasyc). Для многопоточного доступа не предназначена.
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
### 2.1. Создание очереди
|
||||
|
||||
```c
|
||||
// Без хеша (простая очередь)
|
||||
struct ll_queue* q = queue_new(ua, 0, 0, 0, "my_queue");
|
||||
|
||||
// С хешем — для быстрого поиска по ключу внутри data[]
|
||||
// hash_size=256, ключ лежит в data[] по смещению 0, размер ключа 4 байта
|
||||
struct ll_queue* q = queue_new(ua, 256, 0, 4, "my_hash_queue");
|
||||
```
|
||||
|
||||
### 2.2. Создание элемента
|
||||
|
||||
Три способа, по убыванию производительности:
|
||||
|
||||
```c
|
||||
// 1. Из memory pool (самый быстрый)
|
||||
struct ll_entry* e = queue_entry_new_from_pool(instance->pkt_pool);
|
||||
|
||||
// 2. Через malloc (data[] фиксированного размера одним блоком)
|
||||
struct ll_entry* e = queue_entry_new(sizeof(struct my_data));
|
||||
|
||||
// 3. Элемент + отдельный буфер dgram (для переменного размера данных)
|
||||
struct ll_entry* e = ll_alloc_lldgram(dgram_len);
|
||||
memcpy(e->dgram, src_data, dgram_len);
|
||||
e->len = dgram_len;
|
||||
```
|
||||
|
||||
### 2.3. Запись в очередь (backpressure)
|
||||
|
||||
**Главное правило: не забиваем очередь!** Добавляем следующий элемент только когда очередь опустела до заданного порога. Для этого используется Пороговое ожидание:
|
||||
|
||||
```c
|
||||
// 1. Настроить порог (например: ждать пока count<=0, т.е. очередь совсем пуста)
|
||||
queue_set_threshold(q, 0, 0);
|
||||
|
||||
// 2. В структуре-производителе завести handle:
|
||||
struct queue_waiter_handle my_waiter = {0};
|
||||
|
||||
// 3. При готовности отправить — зарегистрировать ожидание:
|
||||
static void my_send_cb(struct ll_queue* q, void* arg) {
|
||||
struct my_producer* p = (struct my_producer*)arg;
|
||||
struct ll_entry* e = ... // создать элемент
|
||||
e->len = data_len;
|
||||
queue_data_put_with_index(q, e);
|
||||
// Снова зарегистрироваться на следующую порцию
|
||||
queue_waiter_wait(q, &p->waiter, my_send_cb, p);
|
||||
}
|
||||
|
||||
// Первичная регистрация:
|
||||
queue_waiter_wait(q, &my_waiter, my_send_cb, producer);
|
||||
|
||||
// 4. При деинициализации — отменить ожидание:
|
||||
queue_waiter_cancel(q, &my_waiter);
|
||||
```
|
||||
|
||||
### 2.4. Чтение из очереди (callback)
|
||||
|
||||
```c
|
||||
// Установить callback — будет вызываться при появлении элементов
|
||||
queue_set_callback(q, my_callback, my_data);
|
||||
|
||||
static void my_callback(struct ll_queue* q, void* arg) {
|
||||
// 1. ИЗВЛЕЧЬ элемент
|
||||
struct ll_entry* e = queue_data_get(q);
|
||||
if (!e) { queue_resume_callback(q); return; }
|
||||
|
||||
// 2. ОБРАБОТАТЬ
|
||||
if (e->dgram && e->len > 0) {
|
||||
do_something(e->dgram, e->len);
|
||||
queue_dgram_free(e); // сначала освободить dgram
|
||||
}
|
||||
queue_entry_free(e); // потом освободить сам entry
|
||||
|
||||
// 3. ОБЯЗАТЕЛЬНО возобновить callback
|
||||
queue_resume_callback(q);
|
||||
}
|
||||
```
|
||||
|
||||
**Критически важно:** `queue_data_get()` автоматически приостанавливает callback-и (`callback_suspended = 1`). Без вызова `queue_resume_callback()` очередь **навсегда застрянет** — callback больше никогда не вызовется.
|
||||
|
||||
Во **всех ветках** (включая ранние return по ошибке) должен быть `queue_resume_callback(q)`:
|
||||
|
||||
```c
|
||||
// ПРАВИЛЬНО:
|
||||
if (!e) { queue_resume_callback(q); return; }
|
||||
if (!e->dgram) { queue_dgram_free(e); queue_entry_free(e); queue_resume_callback(q); return; }
|
||||
|
||||
// НЕПРАВИЛЬНО — очередь зависнет:
|
||||
if (!e) return;
|
||||
```
|
||||
|
||||
### 2.5. Поиск по индексу
|
||||
|
||||
```c
|
||||
// Создать очередь с хешем (ключ 4 байта по смещению 0):
|
||||
struct ll_queue* q = queue_new(ua, 256, 0, 4, "indexed_q");
|
||||
|
||||
// Добавлять элементы через _with_index (хеш вычисляется автоматически):
|
||||
queue_data_put_with_index(q, entry);
|
||||
|
||||
// Искать:
|
||||
uint32_t key = 42;
|
||||
struct ll_entry* found = queue_find_data_by_index(q, &key);
|
||||
|
||||
// Если несколько элементов с одним ключом — продолжать поиск:
|
||||
struct ll_entry* next = queue_find_next_by_index(q, &key, found);
|
||||
```
|
||||
|
||||
### 2.6. Освобождение ресурсов
|
||||
|
||||
```c
|
||||
// 1. Снять callback
|
||||
queue_set_callback(q, NULL, NULL);
|
||||
|
||||
// 2. Отменить всех waiter-ов
|
||||
queue_waiter_cancel(q, &my_waiter);
|
||||
|
||||
// 3. Извлечь и освободить ВСЕ оставшиеся элементы
|
||||
struct ll_entry* e;
|
||||
while ((e = queue_data_get(q)) != NULL) {
|
||||
queue_dgram_free(e);
|
||||
queue_entry_free(e);
|
||||
}
|
||||
// После извлечения всех элементов resume не нужен — callback уже снят.
|
||||
|
||||
// 4. Освободить очередь
|
||||
queue_free(q);
|
||||
```
|
||||
|
||||
**Важно:** `queue_free()` **не освобождает элементы**. Их нужно извлечь через `queue_data_get()` и освободить вручную.
|
||||
|
||||
### 2.7. Приоритетная вставка (LIFO)
|
||||
|
||||
```c
|
||||
// Вставить в начало очереди (высокий приоритет):
|
||||
queue_data_put_first(q, entry);
|
||||
queue_data_put_first_with_index(q, entry);
|
||||
```
|
||||
|
||||
### 2.8. Удаление из середины
|
||||
|
||||
```c
|
||||
queue_remove_data(q, entry); // удаляет из очереди, НЕ освобождает память
|
||||
queue_dgram_free(entry); // освободить dgram отдельно
|
||||
queue_entry_free(entry); // освободить entry отдельно
|
||||
```
|
||||
|
||||
После `queue_remove_data()` элемент больше не принадлежит очереди — вызывающий сам отвечает за его освобождение.
|
||||
|
||||
### 2.9. Callback при опустошении
|
||||
|
||||
```c
|
||||
static void on_empty(struct ll_queue* q, void* arg) {
|
||||
// Очередь стала пустой (count==0)
|
||||
// Вызывается однократно, после вызова сбрасывается
|
||||
}
|
||||
|
||||
queue_set_empty_callback(q, on_empty, my_data);
|
||||
```
|
||||
|
||||
## 3. API
|
||||
|
||||
### Создание / уничтожение
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_new(ua, hash_size, index_offset, index_size, name)` | Создать очередь. `hash_size=0` — без хеша. `ua` обязателен (для uasync_call_soon). |
|
||||
| `queue_free(q)` | Освободить очередь (НЕ элементы — их надо извлечь и освободить отдельно). |
|
||||
|
||||
### Создание / освобождение элементов
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_entry_new(data_size)` | Выделить entry с data[] через malloc. |
|
||||
| `queue_entry_new_from_pool(pool)` | Выделить entry из memory pool. |
|
||||
| `ll_alloc_lldgram(len)` | Выделить entry + отдельный буфер dgram (malloc). |
|
||||
| `queue_entry_free(entry)` | Освободить entry (возврат в pool или free). НЕ освобождает dgram! |
|
||||
| `queue_dgram_free(entry)` | Освободить dgram (через dgram_free_fn, pool или free). |
|
||||
|
||||
### Чтение (callback)
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_set_callback(q, cbk_fn, arg)` | Установить callback автозабора. Вызывается при наличии элементов. |
|
||||
| `queue_data_get(q)` | Извлечь элемент из головы. Приостанавливает callback до `queue_resume_callback()`. |
|
||||
| `queue_resume_callback(q)` | **Обязательно** вызвать после обработки элемента. Планирует отложенный вызов callback через uasync_call_soon. |
|
||||
|
||||
### Запись
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_data_put(q, entry)` | Добавить в конец (FIFO). Для очередей БЕЗ хеша. |
|
||||
| `queue_data_put_with_index(q, entry)` | Добавить в конец с хеш-индексом. |
|
||||
| `queue_data_put_first(q, entry)` | Добавить в начало (LIFO, высокий приоритет). |
|
||||
| `queue_data_put_first_with_index(q, entry)` | Добавить в начало с хеш-индексом. |
|
||||
|
||||
### Backpressure
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_set_threshold(q, max_packets, max_bytes)` | Задать общий порог. Когда `count <= max_packets` И `total_bytes <= max_bytes`, waiter-ы пробуждаются (FIFO). |
|
||||
| `queue_waiter_wait(q, h, callback, arg)` | Зарегистрировать ожидание. Возвращает 1 (уже выполнено), 0 (в очереди), -1 (ошибка). |
|
||||
| `queue_waiter_cancel(q, h)` | Отменить ожидание. Безопасно вызывать в любом состоянии. |
|
||||
| `queue_set_waiter_defer(q, enable)` | Отложенный вызов waiter-ов (через uasync_call_soon). |
|
||||
| `queue_set_empty_callback(q, cbk_fn, arg)` | Одноразовый callback при count==0. |
|
||||
| `queue_set_on_get(q, cbk_fn, arg)` | Callback после каждого queue_data_get (deferred). |
|
||||
|
||||
### Поиск / удаление
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_find_data_by_index(q, index_key)` | Найти по ключу (требуется hash_size>0). Размер ключа = q->index_size. |
|
||||
| `queue_find_next_by_index(q, index_key, prev_entry)` | Следующий элемент с тем же ключом. |
|
||||
| `queue_remove_data(q, entry)` | Удалить из очереди (память НЕ освобождает). |
|
||||
|
||||
### Утилиты
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_entry_count(q)` | Текущее количество элементов. |
|
||||
| `queue_total_bytes(q)` | Суммарный объём данных (сумма len). |
|
||||
| `queue_set_size_limit(q, lim)` | Ограничение на количество элементов (при превышении новые удаляются). |
|
||||
| `queue_check_consistency(q)` | Проверка целостности (счётчики, циклы, prev/next). Только для отладки. |
|
||||
@@ -13,11 +13,15 @@
|
||||
#define CANARY 0xDEADBEEF
|
||||
#define PADDING_FILL 0xAA
|
||||
#define METADATA_SIZE 128
|
||||
#define ALLOCATION_OVERHEAD (12 + (size_t)2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE)
|
||||
#define POINTER_SIZE sizeof(void*)
|
||||
#define LOCATION_MAX (METADATA_SIZE - POINTER_SIZE * 2)
|
||||
#define PREV_OFFSET LOCATION_MAX
|
||||
#define NEXT_OFFSET (LOCATION_MAX + POINTER_SIZE)
|
||||
|
||||
// Порог детекта аномально больших аллокаций (гигантские mmap-резервации, напр. VSZ ~4G)
|
||||
#define LARGE_ALLOC_LOG_THRESHOLD (8u * 1024u * 1024u) // 8 МБ
|
||||
|
||||
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
|
||||
#include <execinfo.h>
|
||||
#include <pthread.h>
|
||||
@@ -86,7 +90,7 @@ void u_check(void* ptr, const char* text, const char* location) {
|
||||
uint8_t* prefix_start = base + METADATA_SIZE;
|
||||
uint32_t* pre_canary = (uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE);
|
||||
uint32_t size = *(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE + 4);
|
||||
uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE;
|
||||
size_t total = (size_t)size + ALLOCATION_OVERHEAD;
|
||||
uint8_t* user = (uint8_t*)ptr;
|
||||
bool error = false;
|
||||
for (size_t i = 0; i < BOUNDARY_CHECK_SIZE; i++) {
|
||||
@@ -106,14 +110,15 @@ void u_check(void* ptr, const char* text, const char* location) {
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
uint32_t* post_canary = (uint32_t*)(user + size);
|
||||
if (*post_canary != CANARY) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: overflow canary corrupted at %p (expected 0x%X, got 0x%X)", location, text, ptr, CANARY, *post_canary);
|
||||
uint32_t post_canary;
|
||||
memcpy(&post_canary, user + size, sizeof(post_canary));
|
||||
if (post_canary != CANARY) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: overflow canary corrupted at %p (expected 0x%X, got 0x%X)", location, text, ptr, CANARY, post_canary);
|
||||
error = true;
|
||||
}
|
||||
if (error) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Allocated at: %s", location, text, alloc_loc);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Memory block size (user): %u bytes, total allocated: %u bytes", location, text, size, total);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Memory block size (user): %u bytes, total allocated: %zu bytes", location, text, size, total);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Full memory dump at base %p:", location, text, base);
|
||||
hex_dump(base, total);
|
||||
exit(EXIT_FAILURE);
|
||||
@@ -121,9 +126,21 @@ void u_check(void* ptr, const char* text, const char* location) {
|
||||
}
|
||||
|
||||
void* u_malloc_impl(uint32_t size, const char* location) {
|
||||
uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE;
|
||||
size_t total = size;
|
||||
if (total > SIZE_MAX - ALLOCATION_OVERHEAD) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "MALLOC OVERFLOW: %u user bytes + %zu overhead bytes at %s", size, ALLOCATION_OVERHEAD, location);
|
||||
return NULL;
|
||||
}
|
||||
if (size >= LARGE_ALLOC_LOG_THRESHOLD) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "LARGE ALLOC: %u bytes (%.1f MB) at %s",
|
||||
size, (double)size / (1024.0 * 1024.0), location);
|
||||
}
|
||||
total += ALLOCATION_OVERHEAD;
|
||||
uint8_t* base = (uint8_t*)malloc(total);
|
||||
if (!base && size > 0) return NULL;
|
||||
if (!base) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "MALLOC FAILED: %u user bytes, %zu total bytes at %s", size, total, location);
|
||||
return NULL;
|
||||
}
|
||||
memset(base, PADDING_FILL, total);
|
||||
// Set metadata
|
||||
size_t loc_len = strlen(location);
|
||||
@@ -134,7 +151,8 @@ void* u_malloc_impl(uint32_t size, const char* location) {
|
||||
uint8_t* prefix_start = base + METADATA_SIZE;
|
||||
*(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE) = CANARY;
|
||||
*(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE + 4) = size;
|
||||
*(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE + 8 + size) = CANARY;
|
||||
const uint32_t post_canary = CANARY; /* размер пользователя не обязан быть кратен выравниванию uint32_t */
|
||||
memcpy(prefix_start + BOUNDARY_CHECK_SIZE + 8 + size, &post_canary, sizeof(post_canary));
|
||||
// Add to list (thread-safe)
|
||||
LOCK();
|
||||
void** prev_ptr = (void**)(base + PREV_OFFSET);
|
||||
@@ -151,6 +169,10 @@ void* u_malloc_impl(uint32_t size, const char* location) {
|
||||
}
|
||||
|
||||
void* u_calloc_impl(uint32_t nmemb, uint32_t size, const char* location) {
|
||||
if (nmemb != 0 && size > UINT32_MAX / nmemb) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "CALLOC OVERFLOW: %u * %u (wraps uint32) at %s", nmemb, size, location);
|
||||
return NULL;
|
||||
}
|
||||
uint32_t total_size = nmemb * size;
|
||||
void* ptr = u_malloc_impl(total_size, location);
|
||||
if (ptr) memset(ptr, 0, total_size);
|
||||
@@ -196,7 +218,7 @@ void u_free_impl(void* ptr, const char* location) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Base: %p", (void*)base);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Allocated at: %s", alloc_loc);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "User size: %u bytes", size);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Total allocated: %u bytes", size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Total allocated: %zu bytes", (size_t)size + ALLOCATION_OVERHEAD);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Active allocs: %zu", active_count);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "--- Metadata dump at %p (%u bytes) ---", (void*)base, (unsigned)(METADATA_SIZE + BOUNDARY_CHECK_SIZE + 8));
|
||||
hex_dump(base, METADATA_SIZE + BOUNDARY_CHECK_SIZE + 8);
|
||||
@@ -218,7 +240,7 @@ void u_free_impl(void* ptr, const char* location) {
|
||||
allocated_count--;
|
||||
frees_count++;
|
||||
UNLOCK();
|
||||
uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE;
|
||||
size_t total = (size_t)size + ALLOCATION_OVERHEAD;
|
||||
memset(base, 0, total);
|
||||
free(base);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
/**
|
||||
* Memory management layer
|
||||
* Provides wrappers for malloc/realloc/calloc/free with error handling
|
||||
* mem — выделение памяти с защитными полями, местом аллокации и учётом живых блоков.
|
||||
* Использовать u_malloc/u_calloc/u_realloc/u_strdup и парный u_free; обычный free к этим блокам неприменим.
|
||||
* Макросы передают LOCATION автоматически. OOM возвращает NULL; обнаруженное повреждение завершает процесс.
|
||||
* realloc живого блока при ошибке сохраняет его, при size=0 освобождает; NULL обрабатывается как malloc.
|
||||
* Размеры аргументов _impl — uint32_t; до вызова проверить, что исходные size_t помещаются в этот диапазон.
|
||||
* Список аллокаций защищён мьютексом; доступ к одному блоку и чтение счётчика согласует вызывающий.
|
||||
* Защитные поля проверяются для живых блоков; обращение к уже освобождённому/чужому указателю недопустимо.
|
||||
* Отчёт u_report_unfreed_blocks вызывать после остановки потоков, меняющих список аллокаций.
|
||||
*/
|
||||
#ifndef MEM_H
|
||||
#define MEM_H
|
||||
@@ -12,18 +18,30 @@ extern "C" {
|
||||
#define XSTR(s) STR(s)
|
||||
#define STR(s) #s
|
||||
#define LOCATION __FILE__ ":" XSTR(__LINE__)
|
||||
// Проверяет canary/padding живого блока; text/location — строки диагностики. NULL ptr допустим.
|
||||
// Обнаруженное повреждение логируется в SYS и завершает процесс через exit(EXIT_FAILURE).
|
||||
void u_check(void* ptr, const char* text, const char* location);
|
||||
#define u_malloc(size) u_malloc_impl(size, LOCATION)
|
||||
#define u_calloc(nmemb, size) u_calloc_impl(nmemb, size, LOCATION)
|
||||
#define u_realloc(ptr, size) u_realloc_impl(ptr, size, LOCATION)
|
||||
#define u_free(ptr) u_free_impl(ptr, LOCATION)
|
||||
#define u_strdup(s) u_strdup_impl(s, LOCATION)
|
||||
// Выделяет size байт с метаданными; NULL при OOM/переполнении полного размера. size=0 тоже создаёт учтённый блок.
|
||||
// location — обязательная строка места вызова, копируется в метаданные; payload не обнуляется.
|
||||
void* u_malloc_impl(uint32_t size, const char* location);
|
||||
// Выделяет и обнуляет nmemb*size байт; NULL при произведении > UINT32_MAX или ошибке malloc.
|
||||
void* u_calloc_impl(uint32_t nmemb, uint32_t size, const char* location);
|
||||
// NULL ptr → malloc(size), включая size=0. Для живого ptr: size=0 → free и NULL;
|
||||
// иначе новый блок с min(старый размер, size) байтами данных. При OOM возвращает NULL, старый блок сохранён.
|
||||
// Результат сохранять временно: tmp = u_realloc(ptr, size); if (tmp) ptr = tmp; (при size > 0).
|
||||
void* u_realloc_impl(void* ptr, uint32_t size, const char* location);
|
||||
// Проверяет и освобождает живой блок из этих обёрток; NULL допустим. Обычный free к нему неприменим.
|
||||
void u_free_impl(void* ptr, const char* location);
|
||||
// Копирует ASCIIZ-строку (strlen(s)+1 должно помещаться в uint32_t); NULL при NULL s/OOM. Освобождать u_free.
|
||||
char* u_strdup_impl(const char* s, const char* location);
|
||||
// После остановки изменяющих список потоков: статистика и дампы живых блоков; сами блоки не освобождает.
|
||||
void u_report_unfreed_blocks(void);
|
||||
// Число живых блоков, чтение без блокировки; параллельные аллокации/освобождения нужно исключить извне.
|
||||
size_t u_get_allocated_count(void);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# mem (Memory Wrappers с отслеживанием утечек)
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
Обёртки над стандартными `malloc`/`calloc`/`realloc`/`free`/`strdup` с детекцией:
|
||||
- **Buffer overflow/underflow** — каждый блок окружён канарейками (`0xDEADBEEF`) и padding-областями, которые проверяются при `u_free` и `u_realloc`. При повреждении — подробный дамп блока и аварийное завершение.
|
||||
- **Double free** — двусвязный список всех живых аллокаций; повторное освобождение детектируется по отсутствию блока в списке.
|
||||
- **Утечки памяти** — `u_report_unfreed_blocks()` при завершении выводит все незакрытые блоки с местом аллокации (`файл:строка`), размером и hex-дампом данных.
|
||||
|
||||
Функции потокобезопасны (мьютекс/критическая секция).
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
```c
|
||||
#include "mem.h"
|
||||
|
||||
// Вместо malloc/calloc/realloc/free/strdup используй соответствующие макросы:
|
||||
void* buf = u_malloc(data_size);
|
||||
char* str = u_strdup("hello");
|
||||
buf = u_realloc(buf, new_size);
|
||||
u_free(buf);
|
||||
u_free(str);
|
||||
|
||||
// В конце main (перед return) — отчёт о незакрытых блоках:
|
||||
u_report_unfreed_blocks();
|
||||
|
||||
// Текущее число живых аллокаций (для диагностики):
|
||||
size_t count = u_get_allocated_count();
|
||||
```
|
||||
|
||||
**Важно:**
|
||||
- Макросы (`u_malloc`, `u_calloc`, `u_free`, и т.д.) автоматически подставляют `__FILE__:__LINE__` через `LOCATION` — это нужно для диагностики утечек и повреждений. Напрямую вызывай `_impl`-функции только если нужно явно передать location.
|
||||
- `u_realloc(NULL, size)` эквивалентен `u_malloc(size)`, `u_realloc(ptr, 0)` эквивалентен `u_free(ptr)`.
|
||||
- Потокобезопасность гарантирована, но аллокации с метаданными заметно дороже обычного `malloc` — используй `memory_pool` для hot-path объектов.
|
||||
- При обнаружении повреждения (overflow/underflow/double-free) программа завершается через `exit(EXIT_FAILURE)` после вывода отладочной информации.
|
||||
|
||||
## 3. API
|
||||
|
||||
| Функция/макрос | Описание |
|
||||
|---|---|
|
||||
| `u_malloc(size)` | Аналог `malloc` с boundary check и трекингом |
|
||||
| `u_calloc(nmemb, size)` | Аналог `calloc` (выделяет + обнуляет) |
|
||||
| `u_realloc(ptr, size)` | Аналог `realloc`, с проверкой целостности старого блока |
|
||||
| `u_free(ptr)` | Освобождение с проверкой canary, double-free и удалением из списка |
|
||||
| `u_strdup(s)` | Аналог `strdup` через u_malloc |
|
||||
| `u_report_unfreed_blocks()` | Выводит все незакрытые блоки с дампом; вызывается при shutdown |
|
||||
| `u_get_allocated_count()` | Возвращает количество живых аллокаций |
|
||||
| `u_check(ptr, text, location)` | Ручная проверка целостности блока (canary + padding) |
|
||||
+26
-14
@@ -5,47 +5,59 @@
|
||||
#include "memory_pool.h"
|
||||
#include "mem.h"
|
||||
|
||||
// Layout: [user_data: object_size] [canary: 4 bytes] [alloc_loc: pointer] [counter: 1 byte]
|
||||
// Layout: [user_data] [canary: 4] [alloc_loc: pointer] [free_loc: pointer] [counter: 1]
|
||||
#define POOL_CANARY_OFF (pool->object_size)
|
||||
#define POOL_LOC_OFF (pool->object_size + 4)
|
||||
#define POOL_COUNTER_OFF (pool->object_size + 4 + sizeof(const char*))
|
||||
#define POOL_ALLOC_SIZE (pool->object_size + 4 + sizeof(const char*) + 1)
|
||||
#define POOL_FREE_LOC_OFF (pool->object_size + 4 + sizeof(const char*))
|
||||
#define POOL_COUNTER_OFF (pool->object_size + 4 + 2 * sizeof(const char*))
|
||||
#define POOL_ALLOC_SIZE (pool->object_size + 4 + 2 * sizeof(const char*) + 1)
|
||||
#define POOL_CANARY_VAL 0xDEADBEAF
|
||||
|
||||
static void pool_init_tags(struct memory_pool* pool, void* obj, const char* location)
|
||||
{
|
||||
uint32_t* canary = (uint32_t*)((uint8_t*)obj + POOL_CANARY_OFF);
|
||||
const char** loc = (const char**)((uint8_t*)obj + POOL_LOC_OFF);
|
||||
const uint32_t canary = POOL_CANARY_VAL;
|
||||
uint8_t* counter = (uint8_t*)obj + POOL_COUNTER_OFF;
|
||||
*canary = POOL_CANARY_VAL;
|
||||
*loc = location;
|
||||
/* Метаданные следуют за payload произвольного размера и могут быть невыровненными. */
|
||||
memcpy((uint8_t*)obj + POOL_CANARY_OFF, &canary, sizeof(canary));
|
||||
memcpy((uint8_t*)obj + POOL_LOC_OFF, &location, sizeof(location));
|
||||
const char* free_loc = NULL;
|
||||
memcpy((uint8_t*)obj + POOL_FREE_LOC_OFF, &free_loc, sizeof(free_loc));
|
||||
*counter = 1;
|
||||
}
|
||||
|
||||
static void pool_check_and_clear_tags(struct memory_pool* pool, void* obj, const char* location)
|
||||
{
|
||||
uint32_t* canary = (uint32_t*)((uint8_t*)obj + POOL_CANARY_OFF);
|
||||
const char** loc = (const char**)((uint8_t*)obj + POOL_LOC_OFF);
|
||||
uint32_t canary;
|
||||
const char *loc, *free_loc;
|
||||
memcpy(&canary, (uint8_t*)obj + POOL_CANARY_OFF, sizeof(canary));
|
||||
memcpy(&loc, (uint8_t*)obj + POOL_LOC_OFF, sizeof(loc));
|
||||
memcpy(&free_loc, (uint8_t*)obj + POOL_FREE_LOC_OFF, sizeof(free_loc));
|
||||
uint8_t* counter = (uint8_t*)obj + POOL_COUNTER_OFF;
|
||||
|
||||
if (*canary != POOL_CANARY_VAL) {
|
||||
if (canary != POOL_CANARY_VAL) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free BUFFER OVERFLOW pool=%p name=%s sz=%zu allocs=%zu reuse=%zu free=%d",
|
||||
pool, pool->name ? pool->name : "?", pool->object_size, pool->allocations, pool->reuse_count, pool->free_count);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, " obj=%p alloc=%s free=%s canary=0x%08x expected=0x%08x",
|
||||
obj, *loc ? *loc : "(null)", location, *canary, POOL_CANARY_VAL);
|
||||
/* Повреждённый хвост может содержать битые указатели; их нельзя читать как строки. */
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, " obj=%p alloc_loc_ptr=%p free_loc_ptr=%p free_attempt=%s canary=0x%08x expected=0x%08x",
|
||||
obj, (void*)loc, (void*)free_loc, location, canary, POOL_CANARY_VAL);
|
||||
if (pool->object_size)
|
||||
log_dump(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_SYS, " user_data", obj, pool->object_size);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free: corruption logged, stopping thread permanently obj=%p", obj);
|
||||
volatile int _halt = 1;
|
||||
while (_halt) {}
|
||||
}
|
||||
if (*counter == 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free DOUBLE FREE pool=%p name=%s sz=%zu allocs=%zu reuse=%zu alloc=%s free=%s",
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free DOUBLE FREE pool=%p name=%s sz=%zu allocs=%zu reuse=%zu obj=%p",
|
||||
pool, pool->name ? pool->name : "?", pool->object_size, pool->allocations, pool->reuse_count,
|
||||
*loc ? *loc : "(null)", location);
|
||||
obj);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, " alloc=%s first_free=%s repeated_free=%s",
|
||||
loc ? loc : "(null)", free_loc ? free_loc : "(null)", location);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free: corruption logged, stopping thread permanently obj=%p", obj);
|
||||
volatile int _halt = 1;
|
||||
while (_halt) {}
|
||||
}
|
||||
*counter = 0;
|
||||
memcpy((uint8_t*)obj + POOL_FREE_LOC_OFF, &location, sizeof(location));
|
||||
}
|
||||
|
||||
static size_t g_total_free = 0;
|
||||
|
||||
+20
-6
@@ -1,4 +1,13 @@
|
||||
// memory_pool.h
|
||||
/* memory_pool — повторное использование блоков фиксированного размера в одном потоке.
|
||||
* init создаёт пустой пул; alloc выделяет блок по требованию или берёт свободный и обнуляет payload.
|
||||
* free проверяет защитный хвост и возвращает не более MEMORY_POOL_MAX_FREE блоков в кеш.
|
||||
* object_size должен вмещать void*: свободный блок хранит next в начале payload.
|
||||
* Перед destroy вернуть все выданные блоки: destroy освобождает только кеш и сам пул.
|
||||
* name заимствован до destroy; блок возвращать только своему пулу, внутренней синхронизации нет. */
|
||||
/* Хвост блока хранит canary, места alloc/free и метку занятости. Повреждение или повторный возврат
|
||||
* кешированного блока логируется в SYS и навсегда останавливает текущий поток циклом диагностики.
|
||||
* Пример для очередей: init(sizeof(struct ll_entry) + inline_size, name), затем queue_entry_new_from_pool.
|
||||
* Объекты, уже переданные в u_free при переполнении кеша, больше нельзя проверять или возвращать. */
|
||||
#ifndef MEMORY_POOL_H
|
||||
#define MEMORY_POOL_H
|
||||
|
||||
@@ -20,26 +29,31 @@ struct memory_pool {
|
||||
void* free_head; // Голова linked list свободных блоков (next хранится в начале каждого блока)
|
||||
int free_count; // Количество свободных блоков
|
||||
size_t object_size; // Размер объектов в пуле (пользовательские данные)
|
||||
size_t allocations; // Общее количество аллокаций (включая новые malloc)
|
||||
size_t allocations; // Попытки выделить новый блок, включая неудачные; выдачи из кеша не учитываются
|
||||
size_t reuse_count; // Количество повторных использований из пула
|
||||
uint8_t alloc_tag_counter; // монотонный счётчик для детекции double-free (1 байт)
|
||||
uint8_t alloc_tag_counter; // Не используется; init устанавливает 1. Double-free проверяется по метке в хвосте блока
|
||||
const char* name; // Имя пула для диагностики (напр. "data_pool", "pkt_pool")
|
||||
};
|
||||
|
||||
// сам пул:
|
||||
// если используем с элементами ll_queue то не забываем что к object_size надо прибавить sizeof(struct ll_entry)
|
||||
// Создаёт пустой пул: object_size >= sizeof(void*), name живёт до destroy; NULL при OOM.
|
||||
// Для ll_queue размер включает sizeof(struct ll_entry) и пользовательский data[].
|
||||
struct memory_pool* memory_pool_init(size_t object_size, const char* name);
|
||||
// Освобождает кеш и пул, но не выданные объекты; NULL допустим. Все владельцы должны предварительно вернуть блоки.
|
||||
void memory_pool_destroy(struct memory_pool* pool);
|
||||
|
||||
// элементы пула:
|
||||
// Выдаёт обнулённый payload из кеша или нового блока; NULL при NULL pool/OOM. location — место вызова в хвосте.
|
||||
void* memory_pool_alloc_impl(struct memory_pool* pool, const char* location);
|
||||
// Возвращает свой живой obj в кеш или u_free, если кеш заполнен; NULL pool/obj ничего не делает.
|
||||
void memory_pool_free_impl(struct memory_pool* pool, void* obj, const char* location);
|
||||
// Копирует счётчики попыток новых аллокаций и выдач из кеша; любой выход может быть NULL, NULL pool ничего не меняет.
|
||||
void memory_pool_get_stats(struct memory_pool* pool, size_t* allocations, size_t* reuse_count);
|
||||
// Общее число кешированных блоков всех пулов; глобальный счётчик не синхронизирован.
|
||||
size_t memory_pool_get_total_free_blocks(void);
|
||||
|
||||
#define memory_pool_alloc(pool) memory_pool_alloc_impl(pool, PLOCATION)
|
||||
#define memory_pool_free(pool, obj) memory_pool_free_impl(pool, obj, PLOCATION)
|
||||
|
||||
/* Проверяет присутствие в кеше свободных блоков; не определяет состояние уже освобождённого через u_free блока. */
|
||||
int memory_pool_is_freed(struct memory_pool* pool, void* obj);
|
||||
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# memory_pool (Object Pool Allocator)
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
Быстрый аллокатор объектов фиксированного размера. Вместо постоянных `malloc`/`free` хранит до 64 освобождённых объектов в linked list для повторного использования. Применяется для hot-path объектов, которые часто создаются и уничтожаются: пакеты, фрагменты, ACK-пакеты, структуры маршрутизации, TCP-сегменты lwIP и т.д.
|
||||
|
||||
Встроенная защита:
|
||||
- **Buffer overflow** — канарейка `0xDEADBEAF` после пользовательских данных проверяется при освобождении. При повреждении — бесконечный цикл (hang) для отладки.
|
||||
- **Double free** — однобайтный счётчик в метаданных; повторное освобождение детектируется и вызывает hang.
|
||||
- Вся диагностика пишется через `DEBUG_ERROR(DEBUG_CATEGORY_MEMORY, ...)`.
|
||||
|
||||
Не потокобезопасен — каждый пул используется из одного потока (обычно в рамках одного u_async event loop).
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
```c
|
||||
#include "memory_pool.h"
|
||||
|
||||
// 1. Создать пул (обычно в init-функции инстанса):
|
||||
struct memory_pool* pkt_pool = memory_pool_init(sizeof(struct ETCP_DGRAM), "pkt_pool");
|
||||
|
||||
// 2. Выделить объект (если есть свободный — вернёт из пула, иначе u_calloc):
|
||||
struct ETCP_DGRAM* dgram = memory_pool_alloc(pkt_pool);
|
||||
|
||||
// 3. Освободить объект (возвращается в пул, если там меньше 64 блоков):
|
||||
memory_pool_free(pkt_pool, dgram);
|
||||
|
||||
// 4. Получить статистику:
|
||||
size_t allocs, reuse;
|
||||
memory_pool_get_stats(pkt_pool, &allocs, &reuse);
|
||||
// reuse много → эффективно; reuse мало → пул слишком мал для нагрузки
|
||||
|
||||
// 5. Проверить, в пуле ли объект (0 — не в пуле, 1 — в пуле):
|
||||
if (memory_pool_is_freed(pkt_pool, obj)) { /* уже освобождён */ }
|
||||
|
||||
// 6. Уничтожить пул (при shutdown):
|
||||
memory_pool_destroy(pkt_pool);
|
||||
// реально освобождает все кэшированные блоки через u_free() и сам пул.
|
||||
```
|
||||
|
||||
**Важно:**
|
||||
- Если объекты пула используются как элементы `ll_queue`, в `object_size` нужно закладывать `sizeof(struct ll_entry)`. Например: `memory_pool_init(sizeof(struct ll_entry) + sizeof(struct dummynet_pkt), "pkt_pool")`.
|
||||
- `memory_pool_alloc` обнуляет объект перед возвратом (защита от утечки старых данных).
|
||||
- При заполнении пула (64 свободных блока) лишние `memory_pool_free` реально вызывают `u_free` — блок не кэшируется.
|
||||
- `memory_pool_destroy` проходит по всем свободным блокам и вызывает `u_free` для каждого, затем освобождает сам `struct memory_pool`.
|
||||
|
||||
## 3. API
|
||||
|
||||
| Функция/макрос | Описание |
|
||||
|---|---|
|
||||
| `memory_pool_init(object_size, name)` | Создаёт пул для объектов заданного размера. `name` — для диагностики. |
|
||||
| `memory_pool_alloc(pool)` | Выделяет объект: из кэша (если есть) или через `u_calloc`. Обнуляет перед возвратом. |
|
||||
| `memory_pool_free(pool, obj)` | Возвращает объект в пул (до 64) или вызывает `u_free`. Проверяет canary и double-free. |
|
||||
| `memory_pool_destroy(pool)` | Уничтожает пул: освобождает все кэшированные блоки и сам пул. |
|
||||
| `memory_pool_get_stats(pool, &allocs, &reuse)` | Статистика: общее число аллокаций и число повторных использований из кэша. |
|
||||
| `memory_pool_get_total_free_blocks()` | Глобальный счётчик свободных блоков во всех пулах (для диагностики). |
|
||||
| `memory_pool_is_freed(pool, obj)` | Проверяет, находится ли объект в свободном списке пула (уже освобождён). |
|
||||
+206
-48
@@ -6780,7 +6780,9 @@ typedef enum
|
||||
ma_device_notification_type_rerouted,
|
||||
ma_device_notification_type_interruption_began,
|
||||
ma_device_notification_type_interruption_ended,
|
||||
ma_device_notification_type_unlocked
|
||||
ma_device_notification_type_unlocked,
|
||||
ma_device_notification_type_capture_overrun, /* uTun: подтверждённый backend разрыв входного PCM. */
|
||||
ma_device_notification_type_playback_underrun /* uTun: backend исчерпал буфер воспроизведения. */
|
||||
} ma_device_notification_type;
|
||||
|
||||
typedef struct
|
||||
@@ -7543,11 +7545,13 @@ struct ma_context
|
||||
ma_proc pa_context_disconnect;
|
||||
ma_proc pa_context_set_state_callback;
|
||||
ma_proc pa_context_get_state;
|
||||
ma_proc pa_context_errno;
|
||||
ma_proc pa_context_get_sink_info_list;
|
||||
ma_proc pa_context_get_source_info_list;
|
||||
ma_proc pa_context_get_sink_info_by_name;
|
||||
ma_proc pa_context_get_source_info_by_name;
|
||||
ma_proc pa_operation_unref;
|
||||
ma_proc pa_operation_cancel;
|
||||
ma_proc pa_operation_get_state;
|
||||
ma_proc pa_channel_map_init_extend;
|
||||
ma_proc pa_channel_map_valid;
|
||||
@@ -7563,6 +7567,7 @@ struct ma_context
|
||||
ma_proc pa_stream_get_buffer_attr;
|
||||
ma_proc pa_stream_set_buffer_attr;
|
||||
ma_proc pa_stream_get_device_name;
|
||||
ma_proc pa_stream_get_latency;
|
||||
ma_proc pa_stream_set_write_callback;
|
||||
ma_proc pa_stream_set_read_callback;
|
||||
ma_proc pa_stream_set_suspended_callback;
|
||||
@@ -7574,6 +7579,7 @@ struct ma_context
|
||||
ma_proc pa_stream_cork;
|
||||
ma_proc pa_stream_trigger;
|
||||
ma_proc pa_stream_begin_write;
|
||||
ma_proc pa_stream_cancel_write;
|
||||
ma_proc pa_stream_write;
|
||||
ma_proc pa_stream_peek;
|
||||
ma_proc pa_stream_drop;
|
||||
@@ -11553,6 +11559,17 @@ IMPLEMENTATION
|
||||
#ifndef miniaudio_c
|
||||
#define miniaudio_c
|
||||
|
||||
/* uTun: необязательные наблюдатели, без изменения ABI и поведения аудиотракта. */
|
||||
#ifndef MA_DIAGNOSTIC_EVENT
|
||||
#define MA_DIAGNOSTIC_EVENT(device, kind, stage, value, detail) ((void)0)
|
||||
#endif
|
||||
#ifndef MA_DIAGNOSTIC_DEVICE_INIT
|
||||
#define MA_DIAGNOSTIC_DEVICE_INIT(device) ((void)0)
|
||||
#endif
|
||||
#ifndef MA_DIAGNOSTIC_LOG
|
||||
#define MA_DIAGNOSTIC_LOG(log, level, message) ((void)0)
|
||||
#endif
|
||||
|
||||
#include <assert.h>
|
||||
#include <limits.h> /* For INT_MAX */
|
||||
#include <math.h> /* sin(), etc. */
|
||||
@@ -13752,6 +13769,7 @@ MA_API ma_result ma_log_post(ma_log* pLog, ma_uint32 level, const char* pMessage
|
||||
return MA_INVALID_ARGS;
|
||||
}
|
||||
|
||||
MA_DIAGNOSTIC_LOG(pLog, level, pMessage);
|
||||
ma_log_lock(pLog);
|
||||
{
|
||||
ma_uint32 iLog;
|
||||
@@ -20200,6 +20218,14 @@ static void ma_device__on_notification(ma_device_notification notification)
|
||||
{
|
||||
MA_ASSERT(notification.pDevice != NULL);
|
||||
|
||||
if (notification.type == ma_device_notification_type_capture_overrun) {
|
||||
MA_DIAGNOSTIC_EVENT(notification.pDevice, AUDIO_DIAG_BACKEND_OVERFLOW, AUDIO_DIAG_READ, 1, 0);
|
||||
} else if (notification.type == ma_device_notification_type_playback_underrun) {
|
||||
MA_DIAGNOSTIC_EVENT(notification.pDevice, AUDIO_DIAG_BACKEND_UNDERRUN, AUDIO_DIAG_WRITE, 1, 0);
|
||||
} else {
|
||||
MA_DIAGNOSTIC_EVENT(notification.pDevice, AUDIO_DIAG_NOTIFY, -1, notification.type, ma_device_get_state(notification.pDevice));
|
||||
}
|
||||
|
||||
if (notification.pDevice->onNotification != NULL) {
|
||||
notification.pDevice->onNotification(¬ification);
|
||||
}
|
||||
@@ -20360,6 +20386,7 @@ static void ma_device__handle_data_callback(ma_device* pDevice, void* pFramesOut
|
||||
{
|
||||
float masterVolumeFactor;
|
||||
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_CALLBACK, frameCount, (pFramesIn != NULL) | ((pFramesOut != NULL) << 1));
|
||||
ma_device_get_master_volume(pDevice, &masterVolumeFactor); /* Use ma_device_get_master_volume() to ensure the volume is loaded atomically. */
|
||||
|
||||
if (pDevice->onData) {
|
||||
@@ -20407,6 +20434,7 @@ static void ma_device__handle_data_callback(ma_device* pDevice, void* pFramesOut
|
||||
ma_silence_pcm_frames(pFramesOut, frameCount, pDevice->playback.format, pDevice->playback.channels);
|
||||
}
|
||||
}
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_CALLBACK, frameCount, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -20590,6 +20618,7 @@ static ma_result ma_device__handle_duplex_callback_capture(ma_device* pDevice, m
|
||||
|
||||
if (framesToProcessInClientFormat == 0) {
|
||||
if (ma_pcm_rb_pointer_distance(pRB) == (ma_int32)ma_pcm_rb_get_subbuffer_size(pRB)) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_CAPTURE_DROP, AUDIO_DIAG_READ, framesToProcessInDeviceFormat, ma_pcm_rb_available_read(pRB));
|
||||
break; /* Overrun. Not enough room in the ring buffer for input frame. Excess frames are dropped. */
|
||||
}
|
||||
}
|
||||
@@ -20625,6 +20654,7 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
|
||||
ma_result result;
|
||||
ma_uint8 silentInputFrames[MA_DATA_CONVERTER_STACK_BUFFER_SIZE];
|
||||
ma_uint32 totalFramesReadOut = 0;
|
||||
ma_silence_pcm_frames(pFramesInInternalFormat, frameCount, pDevice->playback.internalFormat, pDevice->playback.internalChannels);
|
||||
|
||||
MA_ASSERT(pDevice != NULL);
|
||||
MA_ASSERT(frameCount > 0);
|
||||
@@ -20636,6 +20666,7 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
|
||||
Sitting in the ring buffer should be captured data from the capture callback in external format. If there's not enough data in there for
|
||||
the whole frameCount frames we just use silence instead for the input data.
|
||||
*/
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_DUPLEX, frameCount, ma_pcm_rb_available_read(pRB));
|
||||
MA_ZERO_MEMORY(silentInputFrames, sizeof(silentInputFrames));
|
||||
|
||||
while (totalFramesReadOut < frameCount && ma_device_is_started(pDevice)) {
|
||||
@@ -20646,8 +20677,12 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
|
||||
if (pDevice->playback.inputCacheRemaining > 0) {
|
||||
ma_uint64 framesConvertedIn = pDevice->playback.inputCacheRemaining;
|
||||
ma_uint64 framesConvertedOut = (frameCount - totalFramesReadOut);
|
||||
ma_data_converter_process_pcm_frames(&pDevice->playback.converter, ma_offset_pcm_frames_ptr(pDevice->playback.pInputCache, pDevice->playback.inputCacheConsumed, pDevice->playback.format, pDevice->playback.channels), &framesConvertedIn, pFramesInInternalFormat, &framesConvertedOut);
|
||||
result = ma_data_converter_process_pcm_frames(&pDevice->playback.converter, ma_offset_pcm_frames_ptr(pDevice->playback.pInputCache, pDevice->playback.inputCacheConsumed, pDevice->playback.format, pDevice->playback.channels), &framesConvertedIn, pFramesInInternalFormat, &framesConvertedOut);
|
||||
|
||||
if (result != MA_SUCCESS || (framesConvertedIn == 0 && framesConvertedOut == 0)) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_DUPLEX, 1, result);
|
||||
break;
|
||||
}
|
||||
pDevice->playback.inputCacheConsumed += framesConvertedIn;
|
||||
pDevice->playback.inputCacheRemaining -= framesConvertedIn;
|
||||
|
||||
@@ -20662,30 +20697,34 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
|
||||
|
||||
inputFrameCount = (ma_uint32)pDevice->playback.inputCacheCap;
|
||||
result = ma_pcm_rb_acquire_read(pRB, &inputFrameCount, &pInputFrames);
|
||||
if (result == MA_SUCCESS) {
|
||||
if (inputFrameCount > 0) {
|
||||
ma_device__handle_data_callback(pDevice, pDevice->playback.pInputCache, pInputFrames, inputFrameCount);
|
||||
} else {
|
||||
if (ma_pcm_rb_pointer_distance(pRB) == 0) {
|
||||
break; /* Underrun. */
|
||||
}
|
||||
if (result != MA_SUCCESS) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_DUPLEX, 1, result);
|
||||
break;
|
||||
}
|
||||
if (inputFrameCount > 0) {
|
||||
ma_device__handle_data_callback(pDevice, pDevice->playback.pInputCache, pInputFrames, inputFrameCount);
|
||||
result = ma_pcm_rb_commit_read(pRB, inputFrameCount);
|
||||
if (result != MA_SUCCESS) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_DUPLEX, 1, result);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
/* No capture data available. Feed in silence. */
|
||||
inputFrameCount = (ma_uint32)ma_min(pDevice->playback.inputCacheCap, sizeof(silentInputFrames) / ma_get_bytes_per_frame(pDevice->capture.format, pDevice->capture.channels));
|
||||
/* Отсутствие микрофонных кадров не останавливает рендер; синтетический ввод не сдвигает capture-ring. */
|
||||
inputFrameCount = (ma_uint32)ma_min(pDevice->playback.inputCacheCap,
|
||||
sizeof(silentInputFrames) / ma_get_bytes_per_frame(pDevice->capture.format, pDevice->capture.channels));
|
||||
ma_device__handle_data_callback(pDevice, pDevice->playback.pInputCache, silentInputFrames, inputFrameCount);
|
||||
}
|
||||
|
||||
pDevice->playback.inputCacheConsumed = 0;
|
||||
pDevice->playback.inputCacheRemaining = inputFrameCount;
|
||||
|
||||
result = ma_pcm_rb_commit_read(pRB, inputFrameCount);
|
||||
if (result != MA_SUCCESS) {
|
||||
return result; /* Should never happen. */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalFramesReadOut < frameCount) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_UNFILLED, AUDIO_DIAG_DUPLEX, frameCount - totalFramesReadOut, ma_pcm_rb_available_read(pRB));
|
||||
}
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_DUPLEX, totalFramesReadOut, ma_pcm_rb_available_read(pRB));
|
||||
return MA_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -24793,6 +24832,7 @@ static ma_result ma_device_read__wasapi(ma_device* pDevice, void* pFrames, ma_ui
|
||||
|
||||
/* Overrun detection. */
|
||||
if ((flags & MA_AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY) != 0) {
|
||||
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_capture_overrun));
|
||||
/* Glitched. Probably due to an overrun. */
|
||||
|
||||
/*
|
||||
@@ -24923,6 +24963,7 @@ static ma_result ma_device_write__wasapi(ma_device* pDevice, const void* pFrames
|
||||
{
|
||||
ma_result result = MA_SUCCESS;
|
||||
ma_uint32 totalFramesProcessed = 0;
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_WRITE, frameCount, pDevice->wasapi.mappedBufferPlaybackLen);
|
||||
|
||||
/* Keep writing to the device until it's stopped or we've consumed all of our input. */
|
||||
while (ma_device_get_state(pDevice) == ma_device_state_started && totalFramesProcessed < frameCount) {
|
||||
@@ -25009,6 +25050,7 @@ static ma_result ma_device_write__wasapi(ma_device* pDevice, const void* pFrames
|
||||
*pFramesWritten = totalFramesProcessed;
|
||||
}
|
||||
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_WRITE, totalFramesProcessed, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -29869,6 +29911,7 @@ static ma_result ma_device_read__alsa(ma_device* pDevice, void* pFramesOut, ma_u
|
||||
continue; /* Try again. */
|
||||
} else if (resultALSA == -EPIPE) {
|
||||
ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "EPIPE (read)\n");
|
||||
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_capture_overrun));
|
||||
|
||||
/* Overrun. Recover and try again. If this fails we need to return an error. */
|
||||
resultALSA = ((ma_snd_pcm_recover_proc)pDevice->pContext->alsa.snd_pcm_recover)((ma_snd_pcm_t*)pDevice->alsa.pPCMCapture, resultALSA, MA_TRUE);
|
||||
@@ -29924,6 +29967,7 @@ static ma_result ma_device_write__alsa(ma_device* pDevice, const void* pFrames,
|
||||
continue; /* Try again. */
|
||||
} else if (resultALSA == -EPIPE) {
|
||||
ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "EPIPE (write)\n");
|
||||
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_playback_underrun));
|
||||
|
||||
/* Underrun. Recover and try again. If this fails we need to return an error. */
|
||||
resultALSA = ((ma_snd_pcm_recover_proc)pDevice->pContext->alsa.snd_pcm_recover)((ma_snd_pcm_t*)pDevice->alsa.pPCMPlayback, resultALSA, MA_TRUE); /* MA_TRUE=silent (don't print anything on error). */
|
||||
@@ -30908,11 +30952,13 @@ typedef int (* ma_pa_context_connect_proc) (
|
||||
typedef void (* ma_pa_context_disconnect_proc) (ma_pa_context* c);
|
||||
typedef void (* ma_pa_context_set_state_callback_proc) (ma_pa_context* c, ma_pa_context_notify_cb_t cb, void* userdata);
|
||||
typedef ma_pa_context_state_t (* ma_pa_context_get_state_proc) (const ma_pa_context* c);
|
||||
typedef int (* ma_pa_context_errno_proc) (const ma_pa_context* c);
|
||||
typedef ma_pa_operation* (* ma_pa_context_get_sink_info_list_proc) (ma_pa_context* c, ma_pa_sink_info_cb_t cb, void* userdata);
|
||||
typedef ma_pa_operation* (* ma_pa_context_get_source_info_list_proc) (ma_pa_context* c, ma_pa_source_info_cb_t cb, void* userdata);
|
||||
typedef ma_pa_operation* (* ma_pa_context_get_sink_info_by_name_proc) (ma_pa_context* c, const char* name, ma_pa_sink_info_cb_t cb, void* userdata);
|
||||
typedef ma_pa_operation* (* ma_pa_context_get_source_info_by_name_proc)(ma_pa_context* c, const char* name, ma_pa_source_info_cb_t cb, void* userdata);
|
||||
typedef void (* ma_pa_operation_unref_proc) (ma_pa_operation* o);
|
||||
typedef void (* ma_pa_operation_cancel_proc) (ma_pa_operation* o);
|
||||
typedef ma_pa_operation_state_t (* ma_pa_operation_get_state_proc) (const ma_pa_operation* o);
|
||||
typedef ma_pa_channel_map* (* ma_pa_channel_map_init_extend_proc) (ma_pa_channel_map* m, unsigned channels, ma_pa_channel_map_def_t def);
|
||||
typedef int (* ma_pa_channel_map_valid_proc) (const ma_pa_channel_map* m);
|
||||
@@ -30927,6 +30973,7 @@ typedef const ma_pa_sample_spec* (* ma_pa_stream_get_sample_spec_proc) (
|
||||
typedef const ma_pa_channel_map* (* ma_pa_stream_get_channel_map_proc) (ma_pa_stream* s);
|
||||
typedef const ma_pa_buffer_attr* (* ma_pa_stream_get_buffer_attr_proc) (ma_pa_stream* s);
|
||||
typedef ma_pa_operation* (* ma_pa_stream_set_buffer_attr_proc) (ma_pa_stream* s, const ma_pa_buffer_attr* attr, ma_pa_stream_success_cb_t cb, void* userdata);
|
||||
typedef int (* ma_pa_stream_get_latency_proc)(const ma_pa_stream* s, ma_uint64* usec, int* negative);
|
||||
typedef const char* (* ma_pa_stream_get_device_name_proc) (const ma_pa_stream* s);
|
||||
typedef void (* ma_pa_stream_set_write_callback_proc) (ma_pa_stream* s, ma_pa_stream_request_cb_t cb, void* userdata);
|
||||
typedef void (* ma_pa_stream_set_read_callback_proc) (ma_pa_stream* s, ma_pa_stream_request_cb_t cb, void* userdata);
|
||||
@@ -30939,6 +30986,7 @@ typedef int (* ma_pa_stream_is_corked_proc) (
|
||||
typedef ma_pa_operation* (* ma_pa_stream_cork_proc) (ma_pa_stream* s, int b, ma_pa_stream_success_cb_t cb, void* userdata);
|
||||
typedef ma_pa_operation* (* ma_pa_stream_trigger_proc) (ma_pa_stream* s, ma_pa_stream_success_cb_t cb, void* userdata);
|
||||
typedef int (* ma_pa_stream_begin_write_proc) (ma_pa_stream* s, void** data, size_t* nbytes);
|
||||
typedef int (* ma_pa_stream_cancel_write_proc) (ma_pa_stream* s);
|
||||
typedef int (* ma_pa_stream_write_proc) (ma_pa_stream* s, const void* data, size_t nbytes, ma_pa_free_cb_t free_cb, int64_t offset, ma_pa_seek_mode_t seek);
|
||||
typedef int (* ma_pa_stream_peek_proc) (ma_pa_stream* s, const void** data, size_t* nbytes);
|
||||
typedef int (* ma_pa_stream_drop_proc) (ma_pa_stream* s);
|
||||
@@ -31126,23 +31174,49 @@ static ma_pa_channel_position_t ma_channel_position_to_pulse(ma_channel position
|
||||
}
|
||||
#endif
|
||||
|
||||
static ma_result ma_wait_for_operation__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_pa_operation* pOP)
|
||||
#ifndef MA_PULSE_WAIT_TIMEOUT_MS
|
||||
#define MA_PULSE_WAIT_TIMEOUT_MS 1000
|
||||
#endif
|
||||
|
||||
static ma_result ma_pulse_wait_step(ma_context* pContext, ma_ptr pMainLoop, ma_timer* pTimer, const char* operation)
|
||||
{
|
||||
int resultPA;
|
||||
if (ma_timer_get_time_in_seconds(pTimer) * 1000 >= MA_PULSE_WAIT_TIMEOUT_MS) {
|
||||
ma_log_postf(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR,
|
||||
"[PulseAudio] %s timed out after %d ms.\n", operation, MA_PULSE_WAIT_TIMEOUT_MS);
|
||||
return MA_TIMEOUT;
|
||||
}
|
||||
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 0, NULL);
|
||||
if (resultPA < 0) {
|
||||
ma_log_postf(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR, "[PulseAudio] %s iterate failed: %d.\n", operation, resultPA);
|
||||
return ma_result_from_pulse(resultPA);
|
||||
}
|
||||
if (resultPA == 0) ma_sleep(1);
|
||||
return MA_SUCCESS;
|
||||
}
|
||||
|
||||
static ma_result ma_wait_for_operation__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_pa_operation* pOP)
|
||||
{
|
||||
ma_result result;
|
||||
ma_timer timer;
|
||||
ma_pa_operation_state_t state;
|
||||
|
||||
MA_ASSERT(pContext != NULL);
|
||||
MA_ASSERT(pOP != NULL);
|
||||
|
||||
ma_timer_init(&timer);
|
||||
for (;;) {
|
||||
state = ((ma_pa_operation_get_state_proc)pContext->pulse.pa_operation_get_state)(pOP);
|
||||
if (state == MA_PA_OPERATION_DONE) return MA_SUCCESS;
|
||||
if (state != MA_PA_OPERATION_RUNNING) {
|
||||
break; /* Done. */
|
||||
ma_log_post(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR, "[PulseAudio] Operation cancelled.\n");
|
||||
return MA_ERROR;
|
||||
}
|
||||
|
||||
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 1, NULL);
|
||||
if (resultPA < 0) {
|
||||
return ma_result_from_pulse(resultPA);
|
||||
result = ma_pulse_wait_step(pContext, pMainLoop, &timer, "wait");
|
||||
if (result != MA_SUCCESS) {
|
||||
((ma_pa_operation_cancel_proc)pContext->pulse.pa_operation_cancel)(pOP);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31165,9 +31239,11 @@ static ma_result ma_wait_for_operation_and_unref__pulse(ma_context* pContext, ma
|
||||
|
||||
static ma_result ma_wait_for_pa_context_to_connect__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_ptr pPulseContext)
|
||||
{
|
||||
int resultPA;
|
||||
ma_result result;
|
||||
ma_timer timer;
|
||||
ma_pa_context_state_t state;
|
||||
|
||||
ma_timer_init(&timer);
|
||||
for (;;) {
|
||||
state = ((ma_pa_context_get_state_proc)pContext->pulse.pa_context_get_state)((ma_pa_context*)pPulseContext);
|
||||
if (state == MA_PA_CONTEXT_READY) {
|
||||
@@ -31179,10 +31255,8 @@ static ma_result ma_wait_for_pa_context_to_connect__pulse(ma_context* pContext,
|
||||
return MA_ERROR;
|
||||
}
|
||||
|
||||
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 1, NULL);
|
||||
if (resultPA < 0) {
|
||||
return ma_result_from_pulse(resultPA);
|
||||
}
|
||||
result = ma_pulse_wait_step(pContext, pMainLoop, &timer, "context connect");
|
||||
if (result != MA_SUCCESS) return result;
|
||||
}
|
||||
|
||||
/* Should never get here. */
|
||||
@@ -31191,9 +31265,11 @@ static ma_result ma_wait_for_pa_context_to_connect__pulse(ma_context* pContext,
|
||||
|
||||
static ma_result ma_wait_for_pa_stream_to_connect__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_ptr pStream)
|
||||
{
|
||||
int resultPA;
|
||||
ma_result result;
|
||||
ma_timer timer;
|
||||
ma_pa_stream_state_t state;
|
||||
|
||||
ma_timer_init(&timer);
|
||||
for (;;) {
|
||||
state = ((ma_pa_stream_get_state_proc)pContext->pulse.pa_stream_get_state)((ma_pa_stream*)pStream);
|
||||
if (state == MA_PA_STREAM_READY) {
|
||||
@@ -31205,10 +31281,8 @@ static ma_result ma_wait_for_pa_stream_to_connect__pulse(ma_context* pContext, m
|
||||
return MA_ERROR;
|
||||
}
|
||||
|
||||
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 1, NULL);
|
||||
if (resultPA < 0) {
|
||||
return ma_result_from_pulse(resultPA);
|
||||
}
|
||||
result = ma_pulse_wait_step(pContext, pMainLoop, &timer, "stream connect");
|
||||
if (result != MA_SUCCESS) return result;
|
||||
}
|
||||
|
||||
return MA_SUCCESS;
|
||||
@@ -31382,10 +31456,13 @@ static ma_result ma_context_get_default_device_index__pulse(ma_context* pContext
|
||||
|
||||
if (deviceType == ma_device_type_playback) {
|
||||
ma_pa_sink_info sinkInfo;
|
||||
MA_ZERO_OBJECT(&sinkInfo);
|
||||
sinkInfo.index = (ma_uint32)-1;
|
||||
result = ma_context_get_sink_info__pulse(pContext, NULL, &sinkInfo);
|
||||
if (result != MA_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
if (sinkInfo.index == (ma_uint32)-1) return MA_NO_DEVICE;
|
||||
|
||||
if (pIndex != NULL) {
|
||||
*pIndex = sinkInfo.index;
|
||||
@@ -31394,10 +31471,13 @@ static ma_result ma_context_get_default_device_index__pulse(ma_context* pContext
|
||||
|
||||
if (deviceType == ma_device_type_capture) {
|
||||
ma_pa_source_info sourceInfo;
|
||||
MA_ZERO_OBJECT(&sourceInfo);
|
||||
sourceInfo.index = (ma_uint32)-1;
|
||||
result = ma_context_get_source_info__pulse(pContext, NULL, &sourceInfo);
|
||||
if (result != MA_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
if (sourceInfo.index == (ma_uint32)-1) return MA_NO_DEVICE;
|
||||
|
||||
if (pIndex != NULL) {
|
||||
*pIndex = sourceInfo.index;
|
||||
@@ -31551,7 +31631,7 @@ static void ma_context_get_device_info_sink_callback__pulse(ma_pa_context* pPuls
|
||||
{
|
||||
ma_context_get_device_info_callback_data__pulse* pData = (ma_context_get_device_info_callback_data__pulse*)pUserData;
|
||||
|
||||
if (endOfList > 0) {
|
||||
if (endOfList != 0 || pInfo == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31588,7 +31668,7 @@ static void ma_context_get_device_info_source_callback__pulse(ma_pa_context* pPu
|
||||
{
|
||||
ma_context_get_device_info_callback_data__pulse* pData = (ma_context_get_device_info_callback_data__pulse*)pUserData;
|
||||
|
||||
if (endOfList > 0) {
|
||||
if (endOfList != 0 || pInfo == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31640,6 +31720,7 @@ static ma_result ma_context_get_device_info__pulse(ma_context* pContext, ma_devi
|
||||
}
|
||||
|
||||
result = ma_context_get_default_device_index__pulse(pContext, deviceType, &callbackData.defaultDeviceIndex);
|
||||
if (result != MA_SUCCESS) return result;
|
||||
|
||||
if (deviceType == ma_device_type_playback) {
|
||||
pOP = ((ma_pa_context_get_sink_info_by_name_proc)pContext->pulse.pa_context_get_sink_info_by_name)((ma_pa_context*)(pContext->pulse.pPulseContext), pDeviceName, ma_context_get_device_info_sink_callback__pulse, &callbackData);
|
||||
@@ -31648,7 +31729,8 @@ static ma_result ma_context_get_device_info__pulse(ma_context* pContext, ma_devi
|
||||
}
|
||||
|
||||
if (pOP != NULL) {
|
||||
ma_wait_for_operation_and_unref__pulse(pContext, pContext->pulse.pMainLoop, pOP);
|
||||
result = ma_wait_for_operation_and_unref__pulse(pContext, pContext->pulse.pMainLoop, pOP);
|
||||
if (result != MA_SUCCESS) goto done;
|
||||
} else {
|
||||
result = MA_ERROR;
|
||||
goto done;
|
||||
@@ -31747,6 +31829,7 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
|
||||
MA_ASSERT(bpf > 0);
|
||||
|
||||
frameCount = byteCount / bpf;
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_READ, frameCount, deviceState);
|
||||
framesProcessed = 0;
|
||||
|
||||
while (ma_device_get_state(pDevice) == ma_device_state_started && framesProcessed < frameCount) {
|
||||
@@ -31756,6 +31839,7 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
|
||||
|
||||
int pulseResult = ((ma_pa_stream_peek_proc)pDevice->pContext->pulse.pa_stream_peek)(pStream, &pMappedPCMFrames, &bytesMapped);
|
||||
if (pulseResult < 0) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_READ, 1, pulseResult);
|
||||
break; /* Failed to map. Abort. */
|
||||
}
|
||||
|
||||
@@ -31764,12 +31848,25 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
|
||||
if (pMappedPCMFrames != NULL) {
|
||||
ma_device_handle_backend_data_callback(pDevice, NULL, pMappedPCMFrames, framesMapped);
|
||||
} else {
|
||||
/* It's a hole. */
|
||||
ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[PulseAudio] ma_device_on_read__pulse: Hole.\n");
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_HOLE, AUDIO_DIAG_READ, framesMapped, 0);
|
||||
ma_uint8 silence[MA_DATA_CONVERTER_STACK_BUFFER_SIZE];
|
||||
ma_uint64 remaining = framesMapped;
|
||||
ma_uint32 capacity = sizeof(silence) / bpf;
|
||||
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_capture_overrun));
|
||||
ma_silence_pcm_frames(silence, capacity, pDevice->capture.internalFormat, pDevice->capture.internalChannels);
|
||||
while (remaining > 0 && ma_device_get_state(pDevice) == ma_device_state_started) {
|
||||
ma_uint32 n = (ma_uint32)ma_min(remaining, capacity);
|
||||
#ifdef MA_AUDIO_CAPTURE_HOLE
|
||||
MA_AUDIO_CAPTURE_HOLE(framesMapped - remaining);
|
||||
#endif
|
||||
ma_device_handle_backend_data_callback(pDevice, NULL, silence, n);
|
||||
remaining -= n;
|
||||
}
|
||||
}
|
||||
|
||||
pulseResult = ((ma_pa_stream_drop_proc)pDevice->pContext->pulse.pa_stream_drop)(pStream);
|
||||
if (pulseResult < 0) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_READ, 1, pulseResult);
|
||||
break; /* Failed to drop the buffer. */
|
||||
}
|
||||
|
||||
@@ -31780,6 +31877,7 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
|
||||
break;
|
||||
}
|
||||
}
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_READ, framesProcessed, 0);
|
||||
}
|
||||
|
||||
static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stream* pStream, ma_uint64* pFramesProcessed)
|
||||
@@ -31798,6 +31896,7 @@ static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stre
|
||||
|
||||
deviceState = ma_device_get_state(pDevice);
|
||||
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_MAP, 0, deviceState);
|
||||
bytesMapped = ((ma_pa_stream_writable_size_proc)pDevice->pContext->pulse.pa_stream_writable_size)(pStream);
|
||||
if (bytesMapped != (size_t)-1) {
|
||||
if (bytesMapped > 0) {
|
||||
@@ -31810,15 +31909,23 @@ static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stre
|
||||
}
|
||||
|
||||
framesMapped = bytesMapped / bpf;
|
||||
if (framesMapped == 0 || pMappedPCMFrames == NULL) {
|
||||
((ma_pa_stream_cancel_write_proc)pDevice->pContext->pulse.pa_stream_cancel_write)(pStream);
|
||||
result = MA_ERROR;
|
||||
goto done;
|
||||
}
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_MAP, framesMapped, 0);
|
||||
|
||||
if (deviceState == ma_device_state_started || deviceState == ma_device_state_starting) { /* Check for starting state just in case this is being used to do the initial fill. */
|
||||
ma_device_handle_backend_data_callback(pDevice, pMappedPCMFrames, NULL, framesMapped);
|
||||
} else {
|
||||
/* Device is not started. Write silence. */
|
||||
ma_silence_pcm_frames(pMappedPCMFrames, framesMapped, pDevice->playback.format, pDevice->playback.channels);
|
||||
ma_silence_pcm_frames(pMappedPCMFrames, framesMapped, pDevice->playback.internalFormat, pDevice->playback.internalChannels);
|
||||
}
|
||||
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_SUBMIT, framesMapped, 0);
|
||||
pulseResult = ((ma_pa_stream_write_proc)pDevice->pContext->pulse.pa_stream_write)(pStream, pMappedPCMFrames, bytesMapped, NULL, 0, MA_PA_SEEK_RELATIVE);
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_SUBMIT, framesMapped, pulseResult);
|
||||
if (pulseResult < 0) {
|
||||
result = ma_result_from_pulse(pulseResult);
|
||||
goto done; /* Failed to write data to stream. */
|
||||
@@ -31835,6 +31942,10 @@ static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stre
|
||||
}
|
||||
|
||||
done:
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_MAP, framesProcessed, result);
|
||||
if (result != MA_SUCCESS) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_WRITE, 1, result);
|
||||
}
|
||||
if (pFramesProcessed != NULL) {
|
||||
*pFramesProcessed = framesProcessed;
|
||||
}
|
||||
@@ -31850,6 +31961,7 @@ static void ma_device_on_write__pulse(ma_pa_stream* pStream, size_t byteCount, v
|
||||
ma_uint64 framesProcessed;
|
||||
ma_uint32 deviceState;
|
||||
ma_result result;
|
||||
ma_bool32 diagnosticNoProgress = MA_FALSE;
|
||||
|
||||
MA_ASSERT(pDevice != NULL);
|
||||
|
||||
@@ -31866,6 +31978,7 @@ static void ma_device_on_write__pulse(ma_pa_stream* pStream, size_t byteCount, v
|
||||
MA_ASSERT(bpf > 0);
|
||||
|
||||
frameCount = byteCount / bpf;
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_WRITE, frameCount, deviceState);
|
||||
framesProcessed = 0;
|
||||
|
||||
while (framesProcessed < frameCount) {
|
||||
@@ -31882,8 +31995,14 @@ static void ma_device_on_write__pulse(ma_pa_stream* pStream, size_t byteCount, v
|
||||
break;
|
||||
}
|
||||
|
||||
if (framesProcessedThisIteration == 0 && !diagnosticNoProgress) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_NO_PROGRESS, AUDIO_DIAG_WRITE, 1, frameCount - framesProcessed);
|
||||
diagnosticNoProgress = MA_TRUE;
|
||||
}
|
||||
if (framesProcessedThisIteration == 0) break;
|
||||
framesProcessed += framesProcessedThisIteration;
|
||||
}
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_WRITE, framesProcessed, 0);
|
||||
}
|
||||
|
||||
static void ma_device_on_suspended__pulse(ma_pa_stream* pStream, void* pUserData)
|
||||
@@ -31902,10 +32021,10 @@ static void ma_device_on_suspended__pulse(ma_pa_stream* pStream, void* pUserData
|
||||
|
||||
if (suspended == 1) {
|
||||
ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[Pulse] Device suspended state changed. Suspended.\n");
|
||||
ma_device__on_notification_stopped(pDevice);
|
||||
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_interruption_began));
|
||||
} else {
|
||||
ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[Pulse] Device suspended state changed. Resumed.\n");
|
||||
ma_device__on_notification_started(pDevice);
|
||||
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_interruption_ended));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32044,7 +32163,7 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi
|
||||
if (pDescriptorCapture->sampleRate != 0) {
|
||||
ss.rate = pDescriptorCapture->sampleRate;
|
||||
}
|
||||
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY;
|
||||
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY | MA_PA_STREAM_AUTO_TIMING_UPDATE | MA_PA_STREAM_INTERPOLATE_TIMING;
|
||||
|
||||
if (ma_format_from_pulse(ss.format) == ma_format_unknown) {
|
||||
if (ma_is_little_endian()) {
|
||||
@@ -32203,7 +32322,7 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi
|
||||
ss.rate = pDescriptorPlayback->sampleRate;
|
||||
}
|
||||
|
||||
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY;
|
||||
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY | MA_PA_STREAM_AUTO_TIMING_UPDATE | MA_PA_STREAM_INTERPOLATE_TIMING;
|
||||
if (ma_format_from_pulse(ss.format) == ma_format_unknown) {
|
||||
if (ma_is_little_endian()) {
|
||||
ss.format = MA_PA_SAMPLE_FLOAT32LE;
|
||||
@@ -32375,6 +32494,10 @@ on_error1:
|
||||
((ma_pa_stream_unref_proc)pDevice->pContext->pulse.pa_stream_unref)((ma_pa_stream*)pDevice->pulse.pStreamCapture);
|
||||
}
|
||||
on_error0:
|
||||
((ma_pa_context_disconnect_proc)pDevice->pContext->pulse.pa_context_disconnect)((ma_pa_context*)pDevice->pulse.pPulseContext);
|
||||
((ma_pa_context_unref_proc)pDevice->pContext->pulse.pa_context_unref)((ma_pa_context*)pDevice->pulse.pPulseContext);
|
||||
((ma_pa_mainloop_free_proc)pDevice->pContext->pulse.pa_mainloop_free)((ma_pa_mainloop*)pDevice->pulse.pMainLoop);
|
||||
MA_ZERO_OBJECT(&pDevice->pulse);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -32504,8 +32627,24 @@ static ma_result ma_device_data_loop__pulse(ma_device* pDevice)
|
||||
the callbacks deal with it.
|
||||
*/
|
||||
while (ma_device_get_state(pDevice) == ma_device_state_started) {
|
||||
ma_pa_context_state_t contextState = ((ma_pa_context_get_state_proc)pDevice->pContext->pulse.pa_context_get_state)((ma_pa_context*)pDevice->pulse.pPulseContext);
|
||||
ma_pa_stream_state_t captureState = pDevice->pulse.pStreamCapture ?
|
||||
((ma_pa_stream_get_state_proc)pDevice->pContext->pulse.pa_stream_get_state)((ma_pa_stream*)pDevice->pulse.pStreamCapture) : MA_PA_STREAM_READY;
|
||||
ma_pa_stream_state_t playbackState = pDevice->pulse.pStreamPlayback ?
|
||||
((ma_pa_stream_get_state_proc)pDevice->pContext->pulse.pa_stream_get_state)((ma_pa_stream*)pDevice->pulse.pStreamPlayback) : MA_PA_STREAM_READY;
|
||||
if (contextState == MA_PA_CONTEXT_FAILED || contextState == MA_PA_CONTEXT_TERMINATED ||
|
||||
captureState == MA_PA_STREAM_FAILED || captureState == MA_PA_STREAM_TERMINATED ||
|
||||
playbackState == MA_PA_STREAM_FAILED || playbackState == MA_PA_STREAM_TERMINATED) {
|
||||
ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_ERROR,
|
||||
"[PulseAudio] Device failed context=%d capture=%d playback=%d errno=%d.\n", contextState, captureState,
|
||||
playbackState, ((ma_pa_context_errno_proc)pDevice->pContext->pulse.pa_context_errno)((ma_pa_context*)pDevice->pulse.pPulseContext));
|
||||
return MA_ERROR;
|
||||
}
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_LOOP, 0, 0);
|
||||
resultPA = ((ma_pa_mainloop_iterate_proc)pDevice->pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pDevice->pulse.pMainLoop, 1, NULL);
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_LOOP, 0, resultPA);
|
||||
if (resultPA < 0) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_LOOP, 1, resultPA);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -32588,11 +32727,13 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
|
||||
pContext->pulse.pa_context_disconnect = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_disconnect");
|
||||
pContext->pulse.pa_context_set_state_callback = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_set_state_callback");
|
||||
pContext->pulse.pa_context_get_state = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_state");
|
||||
pContext->pulse.pa_context_errno = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_errno");
|
||||
pContext->pulse.pa_context_get_sink_info_list = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_sink_info_list");
|
||||
pContext->pulse.pa_context_get_source_info_list = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_source_info_list");
|
||||
pContext->pulse.pa_context_get_sink_info_by_name = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_sink_info_by_name");
|
||||
pContext->pulse.pa_context_get_source_info_by_name = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_source_info_by_name");
|
||||
pContext->pulse.pa_operation_unref = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_operation_unref");
|
||||
pContext->pulse.pa_operation_cancel = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_operation_cancel");
|
||||
pContext->pulse.pa_operation_get_state = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_operation_get_state");
|
||||
pContext->pulse.pa_channel_map_init_extend = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_channel_map_init_extend");
|
||||
pContext->pulse.pa_channel_map_valid = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_channel_map_valid");
|
||||
@@ -32607,6 +32748,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
|
||||
pContext->pulse.pa_stream_get_channel_map = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_channel_map");
|
||||
pContext->pulse.pa_stream_get_buffer_attr = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_buffer_attr");
|
||||
pContext->pulse.pa_stream_set_buffer_attr = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_set_buffer_attr");
|
||||
pContext->pulse.pa_stream_get_latency = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_latency");
|
||||
pContext->pulse.pa_stream_get_device_name = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_device_name");
|
||||
pContext->pulse.pa_stream_set_write_callback = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_set_write_callback");
|
||||
pContext->pulse.pa_stream_set_read_callback = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_set_read_callback");
|
||||
@@ -32619,6 +32761,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
|
||||
pContext->pulse.pa_stream_cork = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_cork");
|
||||
pContext->pulse.pa_stream_trigger = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_trigger");
|
||||
pContext->pulse.pa_stream_begin_write = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_begin_write");
|
||||
pContext->pulse.pa_stream_cancel_write = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_cancel_write");
|
||||
pContext->pulse.pa_stream_write = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_write");
|
||||
pContext->pulse.pa_stream_peek = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_peek");
|
||||
pContext->pulse.pa_stream_drop = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_drop");
|
||||
@@ -32651,11 +32794,13 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
|
||||
ma_pa_context_disconnect_proc _pa_context_disconnect = pa_context_disconnect;
|
||||
ma_pa_context_set_state_callback_proc _pa_context_set_state_callback = pa_context_set_state_callback;
|
||||
ma_pa_context_get_state_proc _pa_context_get_state = pa_context_get_state;
|
||||
ma_pa_context_errno_proc _pa_context_errno = pa_context_errno;
|
||||
ma_pa_context_get_sink_info_list_proc _pa_context_get_sink_info_list = pa_context_get_sink_info_list;
|
||||
ma_pa_context_get_source_info_list_proc _pa_context_get_source_info_list = pa_context_get_source_info_list;
|
||||
ma_pa_context_get_sink_info_by_name_proc _pa_context_get_sink_info_by_name = pa_context_get_sink_info_by_name;
|
||||
ma_pa_context_get_source_info_by_name_proc _pa_context_get_source_info_by_name= pa_context_get_source_info_by_name;
|
||||
ma_pa_operation_unref_proc _pa_operation_unref = pa_operation_unref;
|
||||
ma_pa_operation_cancel_proc _pa_operation_cancel = pa_operation_cancel;
|
||||
ma_pa_operation_get_state_proc _pa_operation_get_state = pa_operation_get_state;
|
||||
ma_pa_channel_map_init_extend_proc _pa_channel_map_init_extend = pa_channel_map_init_extend;
|
||||
ma_pa_channel_map_valid_proc _pa_channel_map_valid = pa_channel_map_valid;
|
||||
@@ -32670,6 +32815,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
|
||||
ma_pa_stream_get_channel_map_proc _pa_stream_get_channel_map = pa_stream_get_channel_map;
|
||||
ma_pa_stream_get_buffer_attr_proc _pa_stream_get_buffer_attr = pa_stream_get_buffer_attr;
|
||||
ma_pa_stream_set_buffer_attr_proc _pa_stream_set_buffer_attr = pa_stream_set_buffer_attr;
|
||||
ma_pa_stream_get_latency_proc _pa_stream_get_latency = pa_stream_get_latency;
|
||||
ma_pa_stream_get_device_name_proc _pa_stream_get_device_name = pa_stream_get_device_name;
|
||||
ma_pa_stream_set_write_callback_proc _pa_stream_set_write_callback = pa_stream_set_write_callback;
|
||||
ma_pa_stream_set_read_callback_proc _pa_stream_set_read_callback = pa_stream_set_read_callback;
|
||||
@@ -32682,6 +32828,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
|
||||
ma_pa_stream_cork_proc _pa_stream_cork = pa_stream_cork;
|
||||
ma_pa_stream_trigger_proc _pa_stream_trigger = pa_stream_trigger;
|
||||
ma_pa_stream_begin_write_proc _pa_stream_begin_write = pa_stream_begin_write;
|
||||
ma_pa_stream_cancel_write_proc _pa_stream_cancel_write = pa_stream_cancel_write;
|
||||
ma_pa_stream_write_proc _pa_stream_write = pa_stream_write;
|
||||
ma_pa_stream_peek_proc _pa_stream_peek = pa_stream_peek;
|
||||
ma_pa_stream_drop_proc _pa_stream_drop = pa_stream_drop;
|
||||
@@ -32713,11 +32860,13 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
|
||||
pContext->pulse.pa_context_disconnect = (ma_proc)_pa_context_disconnect;
|
||||
pContext->pulse.pa_context_set_state_callback = (ma_proc)_pa_context_set_state_callback;
|
||||
pContext->pulse.pa_context_get_state = (ma_proc)_pa_context_get_state;
|
||||
pContext->pulse.pa_context_errno = (ma_proc)_pa_context_errno;
|
||||
pContext->pulse.pa_context_get_sink_info_list = (ma_proc)_pa_context_get_sink_info_list;
|
||||
pContext->pulse.pa_context_get_source_info_list = (ma_proc)_pa_context_get_source_info_list;
|
||||
pContext->pulse.pa_context_get_sink_info_by_name = (ma_proc)_pa_context_get_sink_info_by_name;
|
||||
pContext->pulse.pa_context_get_source_info_by_name = (ma_proc)_pa_context_get_source_info_by_name;
|
||||
pContext->pulse.pa_operation_unref = (ma_proc)_pa_operation_unref;
|
||||
pContext->pulse.pa_operation_cancel = (ma_proc)_pa_operation_cancel;
|
||||
pContext->pulse.pa_operation_get_state = (ma_proc)_pa_operation_get_state;
|
||||
pContext->pulse.pa_channel_map_init_extend = (ma_proc)_pa_channel_map_init_extend;
|
||||
pContext->pulse.pa_channel_map_valid = (ma_proc)_pa_channel_map_valid;
|
||||
@@ -32732,6 +32881,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
|
||||
pContext->pulse.pa_stream_get_channel_map = (ma_proc)_pa_stream_get_channel_map;
|
||||
pContext->pulse.pa_stream_get_buffer_attr = (ma_proc)_pa_stream_get_buffer_attr;
|
||||
pContext->pulse.pa_stream_set_buffer_attr = (ma_proc)_pa_stream_set_buffer_attr;
|
||||
pContext->pulse.pa_stream_get_latency = (ma_proc)_pa_stream_get_latency;
|
||||
pContext->pulse.pa_stream_get_device_name = (ma_proc)_pa_stream_get_device_name;
|
||||
pContext->pulse.pa_stream_set_write_callback = (ma_proc)_pa_stream_set_write_callback;
|
||||
pContext->pulse.pa_stream_set_read_callback = (ma_proc)_pa_stream_set_read_callback;
|
||||
@@ -32744,6 +32894,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
|
||||
pContext->pulse.pa_stream_cork = (ma_proc)_pa_stream_cork;
|
||||
pContext->pulse.pa_stream_trigger = (ma_proc)_pa_stream_trigger;
|
||||
pContext->pulse.pa_stream_begin_write = (ma_proc)_pa_stream_begin_write;
|
||||
pContext->pulse.pa_stream_cancel_write = (ma_proc)_pa_stream_cancel_write;
|
||||
pContext->pulse.pa_stream_write = (ma_proc)_pa_stream_write;
|
||||
pContext->pulse.pa_stream_peek = (ma_proc)_pa_stream_peek;
|
||||
pContext->pulse.pa_stream_drop = (ma_proc)_pa_stream_drop;
|
||||
@@ -43767,6 +43918,7 @@ MA_API ma_result ma_device_init(ma_context* pContext, const ma_device_config* pC
|
||||
|
||||
result = pContext->callbacks.onDeviceInit(pDevice, pConfig, &descriptorPlayback, &descriptorCapture);
|
||||
if (result != MA_SUCCESS) {
|
||||
ma_event_uninit(&pDevice->stopEvent);
|
||||
ma_event_uninit(&pDevice->startEvent);
|
||||
ma_event_uninit(&pDevice->wakeupEvent);
|
||||
ma_mutex_uninit(&pDevice->startStopLock);
|
||||
@@ -44008,6 +44160,8 @@ MA_API ma_result ma_device_init(ma_context* pContext, const ma_device_config* pC
|
||||
}
|
||||
}
|
||||
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_OPEN, -1, pDevice->type, pDevice->sampleRate);
|
||||
MA_DIAGNOSTIC_DEVICE_INIT(pDevice);
|
||||
MA_ASSERT(ma_device_get_state(pDevice) == ma_device_state_stopped);
|
||||
return MA_SUCCESS;
|
||||
}
|
||||
@@ -44175,6 +44329,7 @@ MA_API void ma_device_uninit(ma_device* pDevice)
|
||||
ma_free(pDevice->pContext, &allocationCallbacks);
|
||||
}
|
||||
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_CLOSE, -1, 0, 0);
|
||||
MA_ZERO_OBJECT(pDevice);
|
||||
}
|
||||
|
||||
@@ -44506,6 +44661,14 @@ MA_API ma_result ma_device_handle_backend_data_callback(ma_device* pDevice, void
|
||||
if (frameCount == 0) {
|
||||
return MA_INVALID_ARGS;
|
||||
}
|
||||
if (((pDevice->type == ma_device_type_capture || pDevice->type == ma_device_type_loopback) && pInput == NULL) ||
|
||||
(pDevice->type == ma_device_type_playback && pOutput == NULL)) {
|
||||
return MA_INVALID_ARGS;
|
||||
}
|
||||
|
||||
#ifdef MA_AUDIO_TIMING_BEGIN
|
||||
MA_AUDIO_TIMING_BEGIN(pDevice, frameCount, pInput != NULL);
|
||||
#endif
|
||||
|
||||
if (pDevice->type == ma_device_type_duplex) {
|
||||
if (pInput != NULL) {
|
||||
@@ -44517,22 +44680,17 @@ MA_API ma_result ma_device_handle_backend_data_callback(ma_device* pDevice, void
|
||||
}
|
||||
} else {
|
||||
if (pDevice->type == ma_device_type_capture || pDevice->type == ma_device_type_loopback) {
|
||||
if (pInput == NULL) {
|
||||
return MA_INVALID_ARGS;
|
||||
}
|
||||
|
||||
ma_device__send_frames_to_client(pDevice, frameCount, pInput);
|
||||
}
|
||||
|
||||
if (pDevice->type == ma_device_type_playback) {
|
||||
if (pOutput == NULL) {
|
||||
return MA_INVALID_ARGS;
|
||||
}
|
||||
|
||||
ma_device__read_frames_from_client(pDevice, frameCount, pOutput);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef MA_AUDIO_TIMING_END
|
||||
MA_AUDIO_TIMING_END();
|
||||
#endif
|
||||
return MA_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -146,6 +146,23 @@ int opus_codec_encoder_signal_set(opus_codec_encoder_t* enc, int signal_type) {
|
||||
return opus_encoder_ctl(enc->opus_enc, OPUS_SET_SIGNAL(signal_type));
|
||||
}
|
||||
|
||||
int opus_codec_encoder_application_set(opus_codec_encoder_t* enc, int application) {
|
||||
if (!enc || !enc->opus_enc) return -1;
|
||||
return opus_encoder_ctl(enc->opus_enc, OPUS_SET_APPLICATION(application));
|
||||
}
|
||||
|
||||
int opus_codec_encoder_delay(opus_codec_encoder_t* enc, int* samples) {
|
||||
if (!enc || !samples) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "Opus delay invalid arguments"); return -1; }
|
||||
opus_int32 delay = 0;
|
||||
int result = opus_encoder_ctl(enc->opus_enc, OPUS_GET_LOOKAHEAD(&delay));
|
||||
if (result != OPUS_OK || delay < 0 || delay > enc->sample_rate) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "Opus delay query failed result=%d samples=%d", result, (int)delay); return -1;
|
||||
}
|
||||
*samples = delay;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "Opus capture delay samples=%d rate=%d", (int)delay, enc->sample_rate);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* opus_codec_strerror(int err) {
|
||||
switch (err) {
|
||||
case OPUS_OK: return "OK";
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/* opus_codec — владеющие обёртки encoder/decoder libopus для int16 interleaved PCM.
|
||||
* create принимает частоту и 1/2 канала; неположительная частота → 48000, неверные каналы → моно.
|
||||
* Encoder/decoder имеют состояние потока; каждому нужен один последовательный владелец.
|
||||
* frame_samples — отсчёты НА КАНАЛ: PCM вмещает frame_samples*channels элементов.
|
||||
* encode возвращает байты Opus, decode — отсчёты на канал; отрицательное значение означает ошибку.
|
||||
* out_cap — байты; decode с data=NULL/len=0 использует PLC libopus. Освобождение — соответствующим destroy. */
|
||||
#ifndef OPUS_CODEC_H
|
||||
#define OPUS_CODEC_H
|
||||
|
||||
@@ -10,6 +16,10 @@ extern "C" {
|
||||
typedef struct opus_codec_encoder opus_codec_encoder_t;
|
||||
typedef struct opus_codec_decoder opus_codec_decoder_t;
|
||||
|
||||
/* Режим кодирования Opus: голос (VOIP, default) или музыка (AUDIO). */
|
||||
#define OPUS_CODEC_APP_VOIP 2048 /* OPUS_APPLICATION_VOIP */
|
||||
#define OPUS_CODEC_APP_AUDIO 2049 /* OPUS_APPLICATION_AUDIO */
|
||||
|
||||
opus_codec_encoder_t* opus_codec_encoder_create(int sample_rate, int channels);
|
||||
void opus_codec_encoder_destroy(opus_codec_encoder_t* enc);
|
||||
int opus_codec_encode(opus_codec_encoder_t* enc, const int16_t* pcm, int frame_samples,
|
||||
@@ -23,6 +33,9 @@ int opus_codec_decode(opus_codec_decoder_t* dec, const uint8_t* data, int len,
|
||||
int opus_codec_encoder_bitrate_set(opus_codec_encoder_t* enc, int bitrate_bps);
|
||||
int opus_codec_encoder_complexity_set(opus_codec_encoder_t* enc, int complexity);
|
||||
int opus_codec_encoder_signal_set(opus_codec_encoder_t* enc, int signal_type);
|
||||
int opus_codec_encoder_application_set(opus_codec_encoder_t* enc, int application);
|
||||
/* Задержка codec в сэмплах на канал; запрашивать после настройки encoder, не задавать константой. */
|
||||
int opus_codec_encoder_delay(opus_codec_encoder_t* enc, int* samples);
|
||||
|
||||
const char* opus_codec_strerror(int err);
|
||||
|
||||
|
||||
@@ -39,6 +39,25 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Классифицирует IPv4-адрес (network byte order):
|
||||
* 1 = глобальный/публичный (маршрутизируемый в интернете),
|
||||
* 0 = приватный, CGNAT, loopback, link-local, multicast или зарезервированный.
|
||||
*/
|
||||
int ip_is_public(uint32_t addr_be) {
|
||||
uint32_t a = ntohl(addr_be);
|
||||
if ((a & 0xFF000000u) == 0x0A000000u) return 0; /* 10.0.0.0/8 */
|
||||
if ((a & 0xFFF00000u) == 0xAC100000u) return 0; /* 172.16.0.0/12 */
|
||||
if ((a & 0xFFFF0000u) == 0xC0A80000u) return 0; /* 192.168.0.0/16 */
|
||||
if ((a & 0xFF000000u) == 0x7F000000u) return 0; /* 127.0.0.0/8 */
|
||||
if ((a & 0xFFFF0000u) == 0xA9FE0000u) return 0; /* 169.254.0.0/16 */
|
||||
if ((a & 0xFF000000u) == 0x00000000u) return 0; /* 0.0.0.0/8 */
|
||||
if ((a & 0xF0000000u) == 0xE0000000u) return 0; /* 224.0.0.0/4 */
|
||||
if ((a & 0xF0000000u) == 0xF0000000u) return 0; /* 240.0.0.0/4 */
|
||||
if ((a & 0xFFC00000u) == 0x64400000u) return 0; /* 100.64.0.0/10 (CGNAT) */
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Generate cryptographically secure random bytes
|
||||
* Returns 0 on success, -1 on error
|
||||
|
||||
+25
-10
@@ -1,5 +1,13 @@
|
||||
/**
|
||||
* Platform compatibility layer for POSIX functions on Windows
|
||||
* platform_compat — общие заголовки POSIX/Windows и адаптеры отсутствующих POSIX-функций.
|
||||
* Также предоставляет криптографический random_bytes, адреса интерфейсов, выбор default route
|
||||
* и классификацию публичных IPv4. Сокетные типы/ошибки и nonblocking API — в socket_compat.h.
|
||||
* Подключать этот заголовок вместо прямых POSIX includes для общего кода.
|
||||
* Windows: htobe16/32/64 и be16/32/64toh через byteswap, strcasecmp/strncasecmp через _stricmp/_strnicmp,
|
||||
* memmem через inline-поиск (пустой образец возвращает начало), poll через WSAPoll, mkdir через _mkdir.
|
||||
* utun_gettimeofday — календарное Unix-время (не монотонное); Windows игнорирует tz.
|
||||
* Windows pipe — _pipe(4096, _O_BINARY). fcntl: F_GETFL возвращает 0, F_SETFL с O_NONBLOCK включает
|
||||
* неблокирующий режим; остальные флаги игнорируются, обратное переключение этим адаптером не выполняется.
|
||||
*/
|
||||
|
||||
#ifndef PLATFORM_COMPAT_H
|
||||
@@ -176,31 +184,38 @@ extern "C" {
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Generate cryptographically secure random bytes
|
||||
// Returns 0 on success, -1 on error
|
||||
// Заполняет buffer криптографическими случайными байтами: POSIX /dev/urandom, Windows BCryptGenRandom.
|
||||
// buffer не NULL, len > 0 (Windows: помещается в ULONG). 0 — всё заполнено, -1 — ошибка/неполное чтение.
|
||||
int random_bytes(uint8_t *buffer, size_t len);
|
||||
|
||||
// Get IPv4 address of interface by its index (ifindex)
|
||||
// Returns IPv4 address in network byte order, 0 on error
|
||||
// POSIX: getifaddrs; Windows: IP Helper. Адрес в network byte order, 0 при ошибке/отсутствии адреса.
|
||||
uint32_t get_interface_ip_by_index(uint32_t netif_index);
|
||||
|
||||
// Get IPv6 address of interface by its index (ifindex)
|
||||
// temporary=1: get temporary (privacy) IPv6, temporary=0: get permanent IPv6
|
||||
// POSIX фильтрует по IFA_F_TEMPORARY в ifa_flags, но getifaddrs не гарантирует адресные флаги;
|
||||
// для точного выбора temporary/stable на Linux использовать get_interface_ipv6_addr_nl().
|
||||
// Windows игнорирует temporary: предпочитает глобальный адрес, затем локальный, исключая link-local/loopback/::.
|
||||
// Returns 0 on success (fills addr_out with 16 bytes), -1 on error or not found
|
||||
int get_interface_ipv6_by_index(uint32_t netif_index, int temporary, uint8_t* addr_out);
|
||||
|
||||
// Get IPv6 address using netlink (Linux-only, accurate IFA_F_* flags)
|
||||
// prefer_stable=1: prefer permanent/stable (IFA_F_MANAGETEMPADDR) over temporary
|
||||
// prefer_stable=0: prefer temporary over stable
|
||||
// Выбрать IPv6: Linux использует netlink с IFA_F_*, прочие POSIX — getifaddrs(), Windows — IP Helper.
|
||||
// POSIX: prefer_stable=1 предпочитает постоянный адрес, 0 — temporary; допускается fallback на другой тип.
|
||||
// Windows игнорирует prefer_stable, выбор как у get_interface_ipv6_by_index().
|
||||
// Returns 0 on success (fills addr_out with 16 bytes), -1 on error
|
||||
int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t* addr_out);
|
||||
|
||||
// Determine the interface index for the default route (outgoing traffic)
|
||||
// Creates a temp UDP socket, connects to a well-known address to force
|
||||
// kernel routing, then matches the chosen source IP to getifaddrs()
|
||||
// POSIX: UDP connect к 8.8.8.8:53 или [2001:4860:4860::8888]:53, сопоставление локального IP с getifaddrs().
|
||||
// Windows: GetBestInterfaceEx к тому же адресу. Результат — выбранный маршрут к этому адресу,
|
||||
// который может отличаться от default route при более специфичных маршрутах.
|
||||
// family: AF_INET or AF_INET6, returns ifindex (0 on failure)
|
||||
uint32_t get_default_route_netif_index(int family);
|
||||
|
||||
// Классифицирует IPv4 (network byte order) по встроенному списку: 0 для 0/8, 10/8, 127/8, 169.254/16,
|
||||
// 172.16/12, 192.168/16, 100.64/10, 224/4 и 240/4; 1 для остальных. Это не проверка достижимости адреса.
|
||||
int ip_is_public(uint32_t addr_be);
|
||||
|
||||
|
||||
// Cross-platform mkdir
|
||||
#ifdef _WIN32
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
# platform_compat — Cross-platform compatibility layer
|
||||
|
||||
## 1. Назначение
|
||||
Унифицирует различия между POSIX (Linux/FreeBSD) и Windows (MSYS2 UCRT64) на уровне системных вызовов и типов. Предоставляет единый API для байтового порядка, строковых функций, энтропии, работы с сетевыми интерфейсами и времени. Полностью заголовочный (inline) для макросов и тривиальных обёрток, `.c` только для нетривиальных реализаций.
|
||||
|
||||
## 2. Как пользоваться
|
||||
Подключить `"platform_compat.h"` — всё остальное разрешается автоматически в зависимости от `_WIN32`.
|
||||
|
||||
### Байтовый порядок
|
||||
```c
|
||||
uint16_t v16 = htobe16(x); // host → big-endian
|
||||
uint32_t v32 = be32toh(x); // big-endian → host
|
||||
uint64_t v64 = be64toh(x); // big-endian → host
|
||||
// На Linux использует <endian.h>, на Windows — _byteswap_*
|
||||
```
|
||||
|
||||
### Криптостойкий random
|
||||
```c
|
||||
uint8_t salt[8];
|
||||
if (random_bytes(salt, sizeof(salt)) != 0) { /* ошибка */ }
|
||||
// Linux: /dev/urandom, Windows: BCryptGenRandom
|
||||
```
|
||||
|
||||
### Сетевые интерфейсы
|
||||
```c
|
||||
// Определить интерфейс маршрута по умолчанию
|
||||
uint32_t ifidx = get_default_route_netif_index(AF_INET); // или AF_INET6
|
||||
|
||||
// Получить IPv4 адрес интерфейса по индексу
|
||||
uint32_t ipv4 = get_interface_ip_by_index(ifidx); // network byte order
|
||||
|
||||
// Получить IPv6 адрес (постоянный или временный)
|
||||
uint8_t ipv6[16];
|
||||
if (get_interface_ipv6_by_index(ifidx, 1, ipv6) == 0) { // temporary=1
|
||||
// ipv6 содержит 16 байт адреса
|
||||
}
|
||||
```
|
||||
**Windows**: `get_interface_ip_by_index`, `get_interface_ipv6_by_index`, `get_default_route_netif_index` — заглушки (возвращают 0/-1).
|
||||
|
||||
### Время
|
||||
```c
|
||||
struct timeval tv;
|
||||
utun_gettimeofday(&tv, NULL); // макрос: на Linux → gettimeofday, на Windows → inline-реализация
|
||||
```
|
||||
|
||||
### Прочее
|
||||
- `strcasecmp`/`strncasecmp` — на Windows `_stricmp`/`_strnicmp`
|
||||
- `memmem` — на Windows inline-реализация `compat_memmem`
|
||||
- `pipe` — на Windows `_pipe` с `_O_BINARY`
|
||||
- `poll` — на Windows `WSAPoll`, флаги `POLLIN`/`POLLOUT` и т.д. определены если отсутствуют
|
||||
- `ssize_t` — определён если отсутствует
|
||||
- `fcntl` — на Windows упрощённая реализация только для `F_SETFL`/`O_NONBLOCK`
|
||||
- `utun_mkdir` — кроссплатформенный `mkdir`
|
||||
|
||||
### Ограничения
|
||||
- Функции сетевых интерфейсов работают только на POSIX, на Windows — заглушки
|
||||
- `get_default_route_netif_index` создаёт временный UDP-сокет к `8.8.8.8` (IPv4) или `2001:4860:4860::8888` (IPv6) для определения интерфейса
|
||||
- Только IPv4/IPv6; `family` только `AF_INET` или `AF_INET6`
|
||||
|
||||
## 3. API
|
||||
|
||||
### Байтовый порядок
|
||||
| Макрос | Назначение |
|
||||
|--------|------------|
|
||||
| `htobe16(x)` / `be16toh(x)` | 16-bit host ↔ big-endian |
|
||||
| `htobe32(x)` / `be32toh(x)` | 32-bit host ↔ big-endian |
|
||||
| `htobe64(x)` / `be64toh(x)` | 64-bit host ↔ big-endian |
|
||||
|
||||
### Основные функции
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `random_bytes(buffer, len)` | Криптостойкие случайные байты. 0 — успех, -1 — ошибка |
|
||||
| `get_interface_ip_by_index(ifindex)` | IPv4 адрес интерфейса в network byte order, 0 при ошибке |
|
||||
| `get_interface_ipv6_by_index(ifindex, temporary, out)` | IPv6 адрес (temporary=1 — временный privacy-адрес), 0 — успех |
|
||||
| `get_default_route_netif_index(family)` | ifindex дефолтного маршрута через connect() к внешнему IP |
|
||||
|
||||
### Макросы-заменители (условные)
|
||||
| Макрос | Назначение |
|
||||
|--------|------------|
|
||||
| `utun_gettimeofday(tv, tz)` | gettimeofday (inline на Windows) |
|
||||
| `utun_mkdir(path, mode)` | mkdir |
|
||||
@@ -0,0 +1,249 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "mem.h"
|
||||
#include "debug_config.h"
|
||||
#include "reed_solomon.h"
|
||||
|
||||
struct rs_codec {
|
||||
unsigned n;
|
||||
unsigned k;
|
||||
size_t max_block_size;
|
||||
uint8_t gf_log[256];
|
||||
uint8_t gf_exp[510];
|
||||
uint8_t *generator;
|
||||
uint8_t *work;
|
||||
uint8_t storage[];
|
||||
};
|
||||
|
||||
/* Умножение в GF(256) с примитивным полиномом x^8+x^4+x^3+x^2+1 (0x11d). */
|
||||
static uint8_t rs_multiply(const struct rs_codec *ctx, uint8_t a, uint8_t b) {
|
||||
return a && b ? ctx->gf_exp[ctx->gf_log[a] + ctx->gf_log[b]] : 0;
|
||||
}
|
||||
|
||||
/* Обратить k строк матрицы; результат — правая половина рабочей матрицы [A|I]. */
|
||||
static int rs_invert(struct rs_codec *ctx, const uint8_t *const rows[]) {
|
||||
unsigned k = ctx->k;
|
||||
unsigned stride = 2 * k;
|
||||
for (unsigned i = 0; i < k; i++) {
|
||||
uint8_t *row = ctx->work + i * stride;
|
||||
memcpy(row, rows[i], k);
|
||||
memset(row + k, 0, k);
|
||||
row[k + i] = 1;
|
||||
}
|
||||
for (unsigned col = 0; col < k; col++) {
|
||||
unsigned pivot = col;
|
||||
while (pivot < k && !ctx->work[pivot * stride + col]) pivot++;
|
||||
if (pivot == k) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: singular matrix n=%u k=%u column=%u", ctx->n, k, col);
|
||||
return RS_ERR_MATRIX;
|
||||
}
|
||||
uint8_t *row = ctx->work + col * stride;
|
||||
if (pivot != col) {
|
||||
uint8_t *other = ctx->work + pivot * stride;
|
||||
for (unsigned j = 0; j < stride; j++) {
|
||||
uint8_t tmp = row[j];
|
||||
row[j] = other[j];
|
||||
other[j] = tmp;
|
||||
}
|
||||
}
|
||||
uint8_t inverse = ctx->gf_exp[255 - ctx->gf_log[row[col]]];
|
||||
for (unsigned j = col; j < stride; j++) row[j] = rs_multiply(ctx, row[j], inverse);
|
||||
for (unsigned i = 0; i < k; i++) {
|
||||
if (i == col) continue;
|
||||
uint8_t *other = ctx->work + i * stride;
|
||||
uint8_t factor = other[col];
|
||||
if (!factor) continue;
|
||||
for (unsigned j = col; j < stride; j++) other[j] ^= rs_multiply(ctx, factor, row[j]);
|
||||
}
|
||||
}
|
||||
return RS_OK;
|
||||
}
|
||||
|
||||
/* Построить систематическую матрицу G=V*inverse(V[0:k]) в точках 2^i поля GF(256). */
|
||||
static int rs_build_generator(struct rs_codec *ctx) {
|
||||
unsigned k = ctx->k;
|
||||
if (ctx->n != k) {
|
||||
const uint8_t *rows[RS_MAX_BLOCKS];
|
||||
for (unsigned i = 0; i < ctx->n; i++) {
|
||||
uint8_t value = 1;
|
||||
for (unsigned j = 0; j < k; j++) {
|
||||
ctx->generator[i * k + j] = value;
|
||||
value = rs_multiply(ctx, value, ctx->gf_exp[i]);
|
||||
}
|
||||
}
|
||||
for (unsigned i = 0; i < k; i++) rows[i] = ctx->generator + i * k;
|
||||
int rc = rs_invert(ctx, rows);
|
||||
if (rc != RS_OK) return rc;
|
||||
for (unsigned i = k; i < ctx->n; i++) {
|
||||
uint8_t result[RS_MAX_BLOCKS];
|
||||
for (unsigned j = 0; j < k; j++) {
|
||||
uint8_t value = 0;
|
||||
for (unsigned t = 0; t < k; t++) {
|
||||
value ^= rs_multiply(ctx, ctx->generator[i * k + t], ctx->work[t * 2 * k + k + j]);
|
||||
}
|
||||
result[j] = value;
|
||||
}
|
||||
memcpy(ctx->generator + i * k, result, k);
|
||||
}
|
||||
}
|
||||
memset(ctx->generator, 0, k * k);
|
||||
for (unsigned i = 0; i < k; i++) ctx->generator[i * k + i] = 1;
|
||||
return RS_OK;
|
||||
}
|
||||
|
||||
/* Инициализация параметров, таблиц поля и матрицы; все рабочие данные в одной аллокации. */
|
||||
int rs_init(struct rs_codec **out, unsigned n, unsigned r, size_t block_size) {
|
||||
if (out) *out = NULL;
|
||||
if (!out || !n || n > RS_MAX_BLOCKS || r >= n || block_size <= RS_HEADER_SIZE || block_size > RS_MAX_BLOCK_SIZE) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: invalid init ctx=%p n=%u r=%u block_size=%zu", (void *)out, n, r, block_size);
|
||||
return RS_ERR_INVALID_ARG;
|
||||
}
|
||||
unsigned k = n - r;
|
||||
size_t bytes = sizeof(struct rs_codec) + n * k + 2 * k * k;
|
||||
struct rs_codec *ctx = u_calloc(1, (uint32_t)bytes);
|
||||
if (!ctx) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: allocation failed n=%u r=%u block_size=%zu bytes=%zu", n, r, block_size, bytes);
|
||||
return RS_ERR_MEMORY;
|
||||
}
|
||||
ctx->n = n;
|
||||
ctx->k = k;
|
||||
ctx->max_block_size = block_size;
|
||||
ctx->generator = ctx->storage;
|
||||
ctx->work = ctx->storage + n * k;
|
||||
unsigned value = 1;
|
||||
for (unsigned i = 0; i < 255; i++) {
|
||||
ctx->gf_exp[i] = (uint8_t)value;
|
||||
ctx->gf_log[value] = (uint8_t)i;
|
||||
value <<= 1;
|
||||
if (value & 256) value ^= 0x11d;
|
||||
}
|
||||
for (unsigned i = 255; i < 510; i++) ctx->gf_exp[i] = ctx->gf_exp[i - 255];
|
||||
int rc = rs_build_generator(ctx);
|
||||
if (rc != RS_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: generator failed n=%u k=%u rc=%d", n, k, rc);
|
||||
u_free(ctx);
|
||||
return rc;
|
||||
}
|
||||
*out = ctx;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "reed_solomon: initialized n=%u r=%u max_block_size=%zu input_capacity=%zu memory=%zu",
|
||||
n, r, block_size, k * (block_size - RS_HEADER_SIZE), bytes);
|
||||
return RS_OK;
|
||||
}
|
||||
|
||||
void rs_free(struct rs_codec *ctx) {
|
||||
if (!ctx) return;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "reed_solomon: free n=%u r=%u max_block_size=%zu", ctx->n, ctx->n - ctx->k, ctx->max_block_size);
|
||||
u_free(ctx);
|
||||
}
|
||||
|
||||
size_t rs_input_capacity(const struct rs_codec *ctx) {
|
||||
if (!ctx) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: input_capacity requires a context");
|
||||
return 0;
|
||||
}
|
||||
return ctx->k * (ctx->max_block_size - RS_HEADER_SIZE);
|
||||
}
|
||||
|
||||
/* Записать заголовки и исходные субблоки, затем вычислить r проверочных субблоков. */
|
||||
int rs_encode(struct rs_codec *ctx, uint64_t seq, const uint8_t *input, size_t length,
|
||||
uint8_t *const blocks[], size_t *block_size) {
|
||||
if (!ctx || !input || !blocks || !block_size || !length || length > rs_input_capacity(ctx)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: invalid encode ctx=%p input=%p blocks=%p length=%zu size_out=%p",
|
||||
(void *)ctx, (const void *)input, (const void *)blocks, length, (void *)block_size);
|
||||
return RS_ERR_INVALID_ARG;
|
||||
}
|
||||
size_t payload_size = (length + ctx->k - 1) / ctx->k;
|
||||
for (unsigned i = 0; i < ctx->n; i++) {
|
||||
if (!blocks[i]) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: missing encode buffer seq=%llu subblock=%u", (unsigned long long)seq, i);
|
||||
return RS_ERR_INVALID_ARG;
|
||||
}
|
||||
}
|
||||
for (unsigned i = 0; i < ctx->n; i++) {
|
||||
for (unsigned j = 0; j < 8; j++) blocks[i][j] = (uint8_t)(seq >> (56 - 8 * j));
|
||||
blocks[i][8] = (uint8_t)i;
|
||||
uint8_t *payload = blocks[i] + RS_HEADER_SIZE;
|
||||
memset(payload, 0, payload_size);
|
||||
if (i < ctx->k) {
|
||||
size_t offset = i * payload_size;
|
||||
size_t take = offset < length ? length - offset : 0;
|
||||
if (take > payload_size) take = payload_size;
|
||||
if (take) memcpy(payload, input + offset, take);
|
||||
continue;
|
||||
}
|
||||
for (unsigned j = 0; j < ctx->k; j++) {
|
||||
uint8_t factor = ctx->generator[i * ctx->k + j];
|
||||
if (!factor) continue;
|
||||
const uint8_t *source = blocks[j] + RS_HEADER_SIZE;
|
||||
for (size_t b = 0; b < payload_size; b++) payload[b] ^= rs_multiply(ctx, factor, source[b]);
|
||||
}
|
||||
}
|
||||
*block_size = RS_HEADER_SIZE + payload_size;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "reed_solomon: encoded seq=%llu n=%u r=%u length=%zu padding=%zu block_size=%zu max=%zu",
|
||||
(unsigned long long)seq, ctx->n, ctx->n - ctx->k, length, ctx->k * payload_size - length,
|
||||
*block_size, ctx->max_block_size);
|
||||
return RS_OK;
|
||||
}
|
||||
|
||||
/* Проверить всю пачку и восстановить только отсутствующие исходные субблоки. */
|
||||
int rs_decode(struct rs_codec *ctx, const uint8_t *const blocks[], size_t block_size,
|
||||
uint8_t *output, size_t output_capacity) {
|
||||
if (!ctx || !blocks || !output || block_size <= RS_HEADER_SIZE || block_size > ctx->max_block_size
|
||||
|| output_capacity < ctx->k * (block_size - RS_HEADER_SIZE)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: invalid decode ctx=%p blocks=%p output=%p block_size=%zu capacity=%zu",
|
||||
(void *)ctx, (const void *)blocks, (void *)output, block_size, output_capacity);
|
||||
return RS_ERR_INVALID_ARG;
|
||||
}
|
||||
size_t payload_size = block_size - RS_HEADER_SIZE;
|
||||
const uint8_t *rows[RS_MAX_BLOCKS];
|
||||
unsigned selected[RS_MAX_BLOCKS];
|
||||
unsigned count = 0;
|
||||
unsigned missing = 0;
|
||||
uint64_t seq = 0;
|
||||
for (unsigned i = 0; i < ctx->n; i++) {
|
||||
if (!blocks[i]) {
|
||||
if (i < ctx->k) missing++;
|
||||
continue;
|
||||
}
|
||||
uint64_t block_seq = 0;
|
||||
for (unsigned j = 0; j < 8; j++) block_seq = (block_seq << 8) | blocks[i][j];
|
||||
if (blocks[i][8] != i || (count && block_seq != seq)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: invalid header slot=%u subblock=%u seq=%llu expected_seq=%llu",
|
||||
i, blocks[i][8], (unsigned long long)block_seq, (unsigned long long)(count ? seq : block_seq));
|
||||
return RS_ERR_HEADER;
|
||||
}
|
||||
seq = block_seq;
|
||||
if (count < ctx->k) {
|
||||
selected[count] = i;
|
||||
rows[count] = ctx->generator + i * ctx->k;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
if (count < ctx->k) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: insufficient blocks seq=%llu received=%u required=%u n=%u",
|
||||
(unsigned long long)seq, count, ctx->k, ctx->n);
|
||||
return RS_ERR_INSUFFICIENT;
|
||||
}
|
||||
if (missing) {
|
||||
int rc = rs_invert(ctx, rows);
|
||||
if (rc != RS_OK) return rc;
|
||||
}
|
||||
for (unsigned i = 0; i < ctx->k; i++) {
|
||||
uint8_t *target = output + i * payload_size;
|
||||
if (blocks[i]) {
|
||||
memcpy(target, blocks[i] + RS_HEADER_SIZE, payload_size);
|
||||
continue;
|
||||
}
|
||||
memset(target, 0, payload_size);
|
||||
for (unsigned j = 0; j < ctx->k; j++) {
|
||||
uint8_t factor = ctx->work[i * 2 * ctx->k + ctx->k + j];
|
||||
if (!factor) continue;
|
||||
const uint8_t *source = blocks[selected[j]] + RS_HEADER_SIZE;
|
||||
for (size_t b = 0; b < payload_size; b++) target[b] ^= rs_multiply(ctx, factor, source[b]);
|
||||
}
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_GENERAL, "reed_solomon: recovered seq=%llu subblock=%u", (unsigned long long)seq, i);
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "reed_solomon: decoded seq=%llu received=%u recovered=%u output_size=%zu",
|
||||
(unsigned long long)seq, count, missing, ctx->k * payload_size);
|
||||
return RS_OK;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Reed–Solomon: кодирование и восстановление одной пачки по известным пропускам.
|
||||
* rs_init задаёт n кодограмм, r проверочных и максимальный размер каждой кодограммы.
|
||||
* Первые n-r кодограмм содержат исходные данные, остальные — проверочные.
|
||||
* Формат: seq (8 байт, big-endian), subblock (1 байт), полезные данные.
|
||||
* blocks[i] всегда соответствует subblock=i; при decode NULL обозначает пропуск.
|
||||
* Любые n-r присутствующих кодограмм восстанавливают всю исходную пачку.
|
||||
* Полученные данные и заголовки должны быть проверены транспортом на повреждения.
|
||||
* Все входные/выходные буферы принадлежат вызывающему коду и не перекрываются.
|
||||
* Кодек выделяет рабочую память только при init; один контекст используют последовательно.
|
||||
* encode принимает действительную длину, выбирает минимальный одинаковый размер частей
|
||||
* и дополняет хвост максимум n-r-1 нулями. Размер и исходную длину хранит вызывающий код.
|
||||
*/
|
||||
#ifndef REED_SOLOMON_H
|
||||
#define REED_SOLOMON_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define RS_HEADER_SIZE 9
|
||||
#define RS_MAX_BLOCK_SIZE 4096
|
||||
#define RS_MAX_BLOCKS 255
|
||||
|
||||
enum rs_result {
|
||||
RS_OK = 0,
|
||||
RS_ERR_INVALID_ARG = -1,
|
||||
RS_ERR_MEMORY = -2,
|
||||
RS_ERR_INSUFFICIENT = -3,
|
||||
RS_ERR_HEADER = -4,
|
||||
RS_ERR_MATRIX = -5
|
||||
};
|
||||
|
||||
struct rs_codec;
|
||||
|
||||
/* Создать кодек: 1 <= n <= 255, 0 <= r < n, 10 <= max_block_size <= 4096.
|
||||
* ctx — выходной указатель; при ошибке *ctx=NULL. Старый контекст сначала освобождают.
|
||||
* Возвращает RS_OK или отрицательный rs_result.
|
||||
*/
|
||||
int rs_init(struct rs_codec **ctx, unsigned n, unsigned r, size_t max_block_size);
|
||||
|
||||
/* Освободить контекст и рабочую память; NULL допустим. */
|
||||
void rs_free(struct rs_codec *ctx);
|
||||
|
||||
/* Максимальная длина входа encode: (n-r)*(max_block_size-RS_HEADER_SIZE).
|
||||
* Для NULL возвращает 0 и логирует ошибку.
|
||||
*/
|
||||
size_t rs_input_capacity(const struct rs_codec *ctx);
|
||||
|
||||
/* Читать 0 < length <= rs_input_capacity(ctx) байт input, записать n кодограмм.
|
||||
* blocks — n ненулевых указателей на буферы ёмкостью max_block_size каждый.
|
||||
* block_size получает RS_HEADER_SIZE+ceil(length/(n-r)); хвост за этим размером не меняется.
|
||||
* При ошибке аргументов выходные буферы и block_size не изменяются.
|
||||
*/
|
||||
int rs_encode(struct rs_codec *ctx, uint64_t seq, const uint8_t *input, size_t length,
|
||||
uint8_t *const blocks[], size_t *block_size);
|
||||
|
||||
/* Восстановить (n-r)*(block_size-RS_HEADER_SIZE) байт, включая нулевой хвост.
|
||||
* Ненулевые буферы содержат block_size байт; NULL обозначает пропуск. Размер общий для пачки.
|
||||
* output_capacity проверяется до записи; block_size не превышает максимум контекста.
|
||||
* Проверяет одинаковый seq и subblock=i у всех присутствующих кодограмм.
|
||||
* Входные буферы не изменяются. При ошибке output не изменяется.
|
||||
*/
|
||||
int rs_decode(struct rs_codec *ctx, const uint8_t *const blocks[], size_t block_size,
|
||||
uint8_t *output, size_t output_capacity);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
+29
-10
@@ -22,17 +22,24 @@
|
||||
* • Все динамические данные (строки, массивы, списки) выделяются через u_malloc.
|
||||
* • При encode длина переменных полей кодируется всегда 2 байтами (uint16_t, big-endian).
|
||||
* • Для linked list в буфере сохраняется только количество узлов + сырые данные узлов
|
||||
* (next-указатели НЕ сериализуются, они восстанавливаются при decode).
|
||||
* (включая байты next/padding; decode заменяет next восстановленными ссылками).
|
||||
* • serialize_decode принимает буфер БЕЗ заголовка.
|
||||
* • serialize_free освобождает ВСЮ структуру и все вложенные динамические объекты.
|
||||
* • Поля в schema.fields должны идти в порядке возрастания offset.
|
||||
* • max_size (если > 0) — жёсткий лимит размера выходного буфера.
|
||||
* • serialize_free освобождает структуру, динамические поля схемы и узлы списков; обнуляет указатель на структуру.
|
||||
* Указатели внутри элементов массивов и узлов списков рекурсивно не освобождаются.
|
||||
* • Поля кодируются в порядке schema.fields; фиксированные данные копируются без смены endian.
|
||||
* • max_size (если > 0) — лимит encode; header при header_len > 0 обязателен.
|
||||
* • При encode counts больше 65535 молча усекаются: сохраняются первые 65535 элементов массива/списка или байтов строки.
|
||||
* • elem_size=1 добавляет NUL и для byte-массивов.
|
||||
* • Linked nodes копируются целиком, без рекурсивного кодирования вложенных указателей.
|
||||
* • Этот формат зависит от ABI сырых полей; сериализация не нормализует размер/выравнивание структур.
|
||||
* • Схема доверенная: offsets, типы, размеры и выравнивание полей/next проверяет её автор, не decode.
|
||||
* • Независимые операции не используют общее изменяемое состояние; доступ к одной структуре согласует владелец.
|
||||
*
|
||||
* @example
|
||||
* // 1. Описание структуры
|
||||
* typedef struct Node {
|
||||
* uint32_t value;
|
||||
* struct Node* next; // next по offset = 4
|
||||
* struct Node* next; // положение задаётся offsetof(Node, next)
|
||||
* } Node;
|
||||
*
|
||||
* typedef struct {
|
||||
@@ -100,7 +107,7 @@ struct SerializeField {
|
||||
uint16_t offset; ///< смещение поля в структуре
|
||||
uint8_t elem_size; ///< размер элемента (для fixed = размер данных, для array/linked = размер одного элемента)
|
||||
uint8_t type; ///< SERIALIZE_TYPE_*
|
||||
uint16_t len_offset; ///< смещение поля-счётчика (игнорируется для type 0,1,2)
|
||||
uint16_t len_offset; ///< type 3–5: смещение счётчика в структуре; type 6: смещение next в узле; type 0–2: игнорируется
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -111,25 +118,37 @@ struct SerializeSchema {
|
||||
uint16_t struct_size; ///< размер основной структуры
|
||||
uint16_t max_size; ///< максимальный размер сериализованных данных (0 = без ограничения)
|
||||
uint16_t header_len; ///< длина заголовка
|
||||
const struct SerializeField* fields;
|
||||
const struct SerializeField* fields; ///< field_count описателей в порядке записи; схема заимствуется на время вызова
|
||||
};
|
||||
|
||||
#define SERIALIZE_ERR_OK 0 ///< успех
|
||||
#define SERIALIZE_ERR_BUF -1 ///< ошибка выделения памяти
|
||||
#define SERIALIZE_ERR_BUF -1 ///< нехватка памяти; decode также: обрезанные данные или неизвестный тип поля
|
||||
#define SERIALIZE_ERR_NULL -2 ///< NULL-указатель
|
||||
#define SERIALIZE_ERR_SIZE -3 ///< превышен max_size
|
||||
#define SERIALIZE_ERR_NOTSUP -4 ///< неподдерживаемый тип
|
||||
#define SERIALIZE_ERR_SIZE -3 ///< encode: превышен max_size
|
||||
#define SERIALIZE_ERR_NOTSUP -4 ///< encode: неподдерживаемый тип поля
|
||||
|
||||
/* Кодирует structure по schema; при header_len > 0 header указывает минимум на header_len байт.
|
||||
* При успехе *out_buf — новый блок u_malloc (освободить u_free), *out_len — фактически записанная длина с header.
|
||||
* Входная структура и её вложенные данные остаются у владельца. На ошибке выходные значения не меняются.
|
||||
* Возврат: OK, NULL (обязательные аргументы), BUF (OOM), SIZE (max_size), NOTSUP (неизвестный тип).
|
||||
* ASCIIZ требует elem_size=1; порядок fields задаёт формат, сортировка по offset не требуется. */
|
||||
int serialize_encode(const void* structure,
|
||||
const struct SerializeSchema* schema,
|
||||
const uint8_t* header,
|
||||
uint8_t** out_buf,
|
||||
size_t* out_len);
|
||||
|
||||
/* Декодирует in_len байт БЕЗ header: передавать buf + header_len, len - header_len.
|
||||
* При успехе *structure — новая структура с выделенными полями; очищать serialize_free с той же схемой.
|
||||
* При ошибке частично созданные объекты освобождаются, *structure = NULL. Старый *structure не освобождает.
|
||||
* max_size не ограничивает decode; хвост после последнего поля игнорируется.
|
||||
* Возврат: OK, NULL (аргументы), BUF (OOM, обрезанные данные или неизвестный тип). */
|
||||
int serialize_decode(const uint8_t* in_buf, size_t in_len,
|
||||
const struct SerializeSchema* schema,
|
||||
void** structure);
|
||||
|
||||
// Освобождает выделенную через u_malloc структуру, поля схемы и узлы списков, обнуляет *structure; NULL допустим.
|
||||
// Вложенные указатели внутри элементов массивов/узлов не обходит; к структуре на стеке применять нельзя.
|
||||
void serialize_free(const struct SerializeSchema* schema, void** structure);
|
||||
|
||||
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
# Binary Serialization Library
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
Библиотека бинарной сериализации C-структур с динамическими полями (строки, массивы, singly-linked списки) в компактный бинарный буфер и обратно. Предназначена для кодирования/декодирования сообщений сетевого протокола (контрольный сервер, синхронизация БД, обмен конфигурацией).
|
||||
|
||||
Что сериализуется:
|
||||
- Фиксированные поля (целые, флаги, структуры) — копируются «как есть».
|
||||
- ASCIIZ-строки (`char*`) — длина определяется через `strlen`, сохраняется с терминальным нулём.
|
||||
- Динамические массивы (`uint8_t*`) — счётчик элементов в поле `UINT8`/`UINT16`/`UINT32` или ровно 1 элемент (`ARRAY_FIXED`).
|
||||
- Singly-linked списки — `next`-указатели не сериализуются, сохраняются только `count+data`.
|
||||
|
||||
Формат: header (`header_len` байт, передаётся отдельно) + данные полей. Длина переменных полей кодируется 2 байтами (`uint16_t`, big-endian).
|
||||
|
||||
Потокобезопасность: нет, однопоточное использование.
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
```c
|
||||
#include "../lib/serialize.h"
|
||||
|
||||
// --- 1. Определяем структуру ---
|
||||
typedef struct Node {
|
||||
uint32_t id;
|
||||
struct Node *next;
|
||||
} Node;
|
||||
|
||||
typedef struct {
|
||||
uint8_t version;
|
||||
uint8_t name_len;
|
||||
char *name; // ASCIIZ (elem_size=1)
|
||||
uint16_t addrs_cnt;
|
||||
uint8_t *addrs; // массив байт (elem_size=1)
|
||||
Node *list; // linked list
|
||||
} Message;
|
||||
|
||||
// --- 2. Описываем схему ---
|
||||
static const struct SerializeField msg_fields[] = {
|
||||
{offsetof(Message, version), sizeof(uint8_t), SERIALIZE_TYPE_FIXED, 0},
|
||||
{offsetof(Message, name), 1, SERIALIZE_TYPE_ARRAY_U8, offsetof(Message, name_len)},
|
||||
{offsetof(Message, addrs), 1, SERIALIZE_TYPE_ARRAY_U16, offsetof(Message, addrs_cnt)},
|
||||
{offsetof(Message, list), sizeof(Node), SERIALIZE_TYPE_LINKED, offsetof(Node, next)}
|
||||
};
|
||||
|
||||
static const struct SerializeSchema msg_schema = {
|
||||
.field_count = 4,
|
||||
.struct_size = sizeof(Message),
|
||||
.max_size = 4096, // 0 = без лимита
|
||||
.header_len = 4, // байты заголовка (напр. версия протокола)
|
||||
.fields = msg_fields
|
||||
};
|
||||
|
||||
// --- 3. Сериализация ---
|
||||
Message msg = { .version = 1, .name = "test", .name_len = 4, .list = NULL };
|
||||
msg.addrs_cnt = 2;
|
||||
msg.addrs = u_malloc(2); msg.addrs[0] = 0xAA; msg.addrs[1] = 0xBB;
|
||||
|
||||
uint8_t header[] = {0x01, 0x00, 0x00, 0x00}; // 4-байтный заголовок
|
||||
uint8_t *buf = NULL;
|
||||
size_t len;
|
||||
if (serialize_encode(&msg, &msg_schema, header, &buf, &len) != SERIALIZE_ERR_OK)
|
||||
goto fail;
|
||||
|
||||
// buf содержит: header (4) + version (1) + [len=2]name(4) + '\0' + [len=2]addrs(2)
|
||||
|
||||
// --- 4. Десериализация (buf без заголовка!) ---
|
||||
Message *restored = NULL;
|
||||
uint8_t *data_ptr = buf + msg_schema.header_len; // пропускаем header
|
||||
if (serialize_decode(data_ptr, len - msg_schema.header_len, &msg_schema, (void**)&restored) != SERIALIZE_ERR_OK)
|
||||
goto fail;
|
||||
|
||||
// restored->name == "test", restored->name_len == 4
|
||||
// restored->addrs[0] == 0xAA, restored->addrs_cnt == 2
|
||||
// restored->list == NULL (список пуст — не падает)
|
||||
|
||||
// --- 5. Очистка ---
|
||||
serialize_free(&msg_schema, (void**)&restored); // освобождает всё: name, addrs, list, структуру
|
||||
u_free(buf);
|
||||
```
|
||||
|
||||
**Ключевые нюансы:**
|
||||
- Поля в `schema.fields` должны идти в порядке возрастания `offset` в структуре.
|
||||
- `serialize_decode` принимает буфер **без заголовка** — передаётся `buf + schema.header_len`.
|
||||
- `serialize_free` освобождает структуру и все вложенные динамические поля (строки, массивы, списки).
|
||||
- При ошибке `serialize_decode` **автоматически освобождает** всю частично выделенную память → `*structure = NULL`.
|
||||
- `max_size > 0` — жёсткий лимит, при превышении `SERIALIZE_ERR_SIZE`.
|
||||
- Длина переменных полей всегда 2 байта (максимум 65535 элементов).
|
||||
- ASCIIZ-строки (`elem_size=1`) сохраняются с терминальным `\0`; пустая строка → 1 байт (`\0`).
|
||||
- Для linked list поле `len_offset` указывает смещение `next`-указателя внутри узла.
|
||||
- Передавать буфер в `serialize_decode` **без заголовка** — он не знает про `header_len`.
|
||||
|
||||
## 3. API
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `serialize_encode(structure, schema, header, &buf, &len)` | Кодирует структуру. `header` копируется в начало `buf`. |
|
||||
| `serialize_decode(in_buf, in_len, schema, &structure)` | Декодирует буфер **без заголовка**. При ошибке всё освобождает. |
|
||||
| `serialize_free(schema, &structure)` | Рекурсивно освобождает структуру и динамические поля, затем обнуляет указатель. |
|
||||
|
||||
**Типы полей (`SERIALIZE_TYPE_*`):**
|
||||
|
||||
| Тип | `data` | `elem_size` | `len_offset` |
|
||||
|-----|--------|-------------|--------------|
|
||||
| `FIXED` (0) | Встроенные данные | Размер поля | Не исп. |
|
||||
| `ASCIIZ` (1) | `char*` | 1 | Не исп. (длина = `strlen`) |
|
||||
| `ARRAY_FIXED` (2) | `uint8_t*` (ровно 1 элемент) | Размер элемента | Не исп. |
|
||||
| `ARRAY_U8` (3) | `uint8_t*` | Размер элемента | Смещение `uint8_t`-счётчика |
|
||||
| `ARRAY_U16` (4) | `uint8_t*` | Размер элемента | Смещение `uint16_t`-счётчика |
|
||||
| `ARRAY_U32` (5) | `uint8_t*` | Размер элемента | Смещение `uint32_t`-счётчика |
|
||||
| `LINKED` (6) | `void*` (голова списка) | Размер узла | Смещение `next` внутри узла |
|
||||
|
||||
**Коды возврата:**
|
||||
- `SERIALIZE_ERR_OK (0)` — успех.
|
||||
- `SERIALIZE_ERR_BUF (-1)` — ошибка выделения памяти.
|
||||
- `SERIALIZE_ERR_NULL (-2)` — NULL-указатель во входных параметрах.
|
||||
- `SERIALIZE_ERR_SIZE (-3)` — превышен `max_size`.
|
||||
- `SERIALIZE_ERR_NOTSUP (-4)` — неподдерживаемый тип поля.
|
||||
@@ -0,0 +1,312 @@
|
||||
#include "silero_vad.h"
|
||||
#include "debug_config.h"
|
||||
#include "mem.h"
|
||||
|
||||
#ifdef HAVE_SILERO_VAD
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <onnxruntime/onnxruntime_c_api.h>
|
||||
|
||||
#include "silero_vad_model.inc"
|
||||
|
||||
/* Число float-элементов рекуррентного состояния модели: 2 * 1 * 128 */
|
||||
#define SILERO_VAD_STATE_COUNT 256
|
||||
/* ONNX-вход: 64 отсчёта предыдущего окна + 512 новых отсчётов @16кГц. */
|
||||
#define SILERO_VAD_CONTEXT_SAMPLES 64
|
||||
#define SILERO_VAD_INPUT_SAMPLES (SILERO_VAD_CONTEXT_SAMPLES + SILERO_VAD_WINDOW_SAMPLES)
|
||||
|
||||
struct silero_vad {
|
||||
const OrtApi* ort;
|
||||
OrtEnv* env;
|
||||
OrtSession* session;
|
||||
OrtMemoryInfo* mem_info;
|
||||
float state[SILERO_VAD_STATE_COUNT]; /* рекуррентное состояние между вызовами */
|
||||
float context[SILERO_VAD_CONTEXT_SAMPLES]; /* звуковой контекст предыдущего окна */
|
||||
uint8_t* model_data; /* байты .onnx, загруженные в память (для file-версии) */
|
||||
size_t model_len;
|
||||
};
|
||||
|
||||
/* Освободить OrtStatus и залогировать его сообщение как ошибку */
|
||||
static void vad_report_ort_error(const OrtApi* ort, OrtStatus* status, const char* what) {
|
||||
const char* msg = ort->GetErrorMessage(status);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "%s: %s", what, msg ? msg : "unknown onnxruntime error");
|
||||
ort->ReleaseStatus(status);
|
||||
}
|
||||
|
||||
/* Прочитать файл целиком в буфер. 0 при успехе: буфер пишется в out/out_len (u_malloc). */
|
||||
static int vad_read_file(const char* path, uint8_t** out, size_t* out_len) {
|
||||
FILE* f;
|
||||
long size;
|
||||
uint8_t* buf;
|
||||
|
||||
*out = NULL;
|
||||
*out_len = 0;
|
||||
|
||||
f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: cannot open model '%s'", path);
|
||||
return -1;
|
||||
}
|
||||
if (fseek(f, 0, SEEK_END) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: fseek failed on '%s'", path);
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
size = ftell(f);
|
||||
if (size <= 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: empty or unreadable model '%s'", path);
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
rewind(f);
|
||||
|
||||
buf = (uint8_t*)u_malloc((size_t)size);
|
||||
if (!buf) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: OOM for %ld bytes", size);
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
if (fread(buf, 1, (size_t)size, f) != (size_t)size) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: short read on '%s'", path);
|
||||
u_free(buf);
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
*out = buf;
|
||||
*out_len = (size_t)size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Общая инициализация детектора из байтов модели (файл или вшитая модель).
|
||||
* owned_buf передаётся детектору только при успехе; при ошибке остаётся у вызывающего.
|
||||
* Для вшитой модели owned_buf == NULL, её байты не освобождаются. */
|
||||
static silero_vad_t* silero_vad_create_from_bytes(const uint8_t* data, size_t len,
|
||||
const uint8_t* owned_buf, const char* label) {
|
||||
silero_vad_t* vad = NULL;
|
||||
const OrtApi* ort;
|
||||
OrtStatus* status;
|
||||
OrtSessionOptions* opts = NULL;
|
||||
|
||||
if (!data || len == 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): empty model", label);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ort = OrtGetApiBase() ? OrtGetApiBase()->GetApi(ORT_API_VERSION) : NULL;
|
||||
if (!ort) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): onnxruntime C API unavailable", label);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
vad = (silero_vad_t*)u_calloc(1, sizeof(*vad));
|
||||
if (!vad) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): OOM", label);
|
||||
return NULL;
|
||||
}
|
||||
vad->ort = ort;
|
||||
vad->model_len = len;
|
||||
|
||||
status = ort->CreateEnv(ORT_LOGGING_LEVEL_WARNING, "utun_silero_vad", &vad->env);
|
||||
if (status) { vad_report_ort_error(ort, status, "CreateEnv"); goto fail; }
|
||||
|
||||
status = ort->CreateSessionOptions(&opts);
|
||||
if (status) { vad_report_ort_error(ort, status, "CreateSessionOptions"); goto fail; }
|
||||
status = ort->SetIntraOpNumThreads(opts, 1);
|
||||
if (status) { vad_report_ort_error(ort, status, "SetIntraOpNumThreads"); }
|
||||
status = ort->SetSessionGraphOptimizationLevel(opts, ORT_ENABLE_ALL);
|
||||
if (status) { vad_report_ort_error(ort, status, "SetSessionGraphOptimizationLevel"); }
|
||||
|
||||
status = ort->CreateSessionFromArray(vad->env, data, len, opts, &vad->session);
|
||||
if (status) { vad_report_ort_error(ort, status, "CreateSessionFromArray"); goto fail; }
|
||||
|
||||
status = ort->CreateCpuMemoryInfo(OrtArenaAllocator, OrtMemTypeDefault, &vad->mem_info);
|
||||
if (status) { vad_report_ort_error(ort, status, "CreateCpuMemoryInfo"); goto fail; }
|
||||
|
||||
ort->ReleaseSessionOptions(opts);
|
||||
vad->model_data = (uint8_t*)owned_buf;
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): ok bytes=%zu window=%d context=%d input=%d", label, len,
|
||||
SILERO_VAD_WINDOW_SAMPLES, SILERO_VAD_CONTEXT_SAMPLES, SILERO_VAD_INPUT_SAMPLES);
|
||||
return vad;
|
||||
|
||||
fail:
|
||||
if (opts) ort->ReleaseSessionOptions(opts);
|
||||
silero_vad_destroy(vad);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
silero_vad_t* silero_vad_create(const char* model_path) {
|
||||
uint8_t* buf = NULL;
|
||||
size_t len = 0;
|
||||
silero_vad_t* vad;
|
||||
|
||||
if (!model_path || !*model_path) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create: empty model path");
|
||||
return NULL;
|
||||
}
|
||||
if (vad_read_file(model_path, &buf, &len) != 0) return NULL;
|
||||
vad = silero_vad_create_from_bytes(buf, len, buf, model_path);
|
||||
if (!vad) u_free(buf);
|
||||
return vad;
|
||||
}
|
||||
|
||||
silero_vad_t* silero_vad_create_default(void) {
|
||||
return silero_vad_create_from_bytes(silero_vad_model_data, silero_vad_model_len, NULL, "embedded");
|
||||
}
|
||||
|
||||
void silero_vad_destroy(silero_vad_t* vad) {
|
||||
if (!vad) return;
|
||||
const OrtApi* ort = vad->ort;
|
||||
if (ort) {
|
||||
if (vad->mem_info) ort->ReleaseMemoryInfo(vad->mem_info);
|
||||
if (vad->session) ort->ReleaseSession(vad->session);
|
||||
if (vad->env) ort->ReleaseEnv(vad->env);
|
||||
}
|
||||
if (vad->model_data) u_free(vad->model_data);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_VAD, "silero_vad_destroy");
|
||||
u_free(vad);
|
||||
}
|
||||
|
||||
void silero_vad_reset(silero_vad_t* vad) {
|
||||
if (!vad) return;
|
||||
memset(vad->state, 0, sizeof(vad->state));
|
||||
memset(vad->context, 0, sizeof(vad->context));
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_VAD, "silero_vad_reset: recurrent state and audio context cleared");
|
||||
}
|
||||
|
||||
int silero_vad_process(silero_vad_t* vad, const float* samples, float* prob) {
|
||||
const OrtApi* ort;
|
||||
OrtStatus* status;
|
||||
OrtValue* in_input = NULL;
|
||||
OrtValue* in_state = NULL;
|
||||
OrtValue* in_sr = NULL;
|
||||
OrtValue* out_prob = NULL;
|
||||
OrtValue* out_state = NULL;
|
||||
const char* input_names[3];
|
||||
const char* output_names[2];
|
||||
const OrtValue* inputs[3];
|
||||
OrtValue* outputs[2];
|
||||
int64_t shape_input[2] = {1, SILERO_VAD_INPUT_SAMPLES};
|
||||
int64_t shape_state[3] = {2, 1, 128};
|
||||
int64_t shape_prob[2] = {1, 1};
|
||||
int64_t sr_value = SILERO_VAD_SAMPLE_RATE;
|
||||
float prob_local = 0.0f;
|
||||
float next_state[SILERO_VAD_STATE_COUNT];
|
||||
float input[SILERO_VAD_INPUT_SAMPLES];
|
||||
int rc = -1;
|
||||
|
||||
if (!vad || !samples || !prob) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_process: invalid args");
|
||||
return -1;
|
||||
}
|
||||
ort = vad->ort;
|
||||
memcpy(input, vad->context, sizeof(vad->context));
|
||||
memcpy(input + SILERO_VAD_CONTEXT_SAMPLES, samples, SILERO_VAD_WINDOW_SAMPLES * sizeof(float));
|
||||
|
||||
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, input,
|
||||
sizeof(input), shape_input, 2,
|
||||
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &in_input);
|
||||
if (status) { vad_report_ort_error(ort, status, "input tensor"); return -1; }
|
||||
|
||||
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, vad->state,
|
||||
SILERO_VAD_STATE_COUNT * sizeof(float), shape_state, 3,
|
||||
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &in_state);
|
||||
if (status) { vad_report_ort_error(ort, status, "state tensor"); goto done; }
|
||||
|
||||
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, &sr_value,
|
||||
sizeof(sr_value), NULL, 0,
|
||||
ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64, &in_sr);
|
||||
if (status) { vad_report_ort_error(ort, status, "sr tensor"); goto done; }
|
||||
|
||||
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, &prob_local,
|
||||
sizeof(float), shape_prob, 2,
|
||||
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &out_prob);
|
||||
if (status) { vad_report_ort_error(ort, status, "output tensor"); goto done; }
|
||||
|
||||
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, next_state,
|
||||
SILERO_VAD_STATE_COUNT * sizeof(float), shape_state, 3,
|
||||
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &out_state);
|
||||
if (status) { vad_report_ort_error(ort, status, "stateN tensor"); goto done; }
|
||||
|
||||
input_names[0] = "input";
|
||||
input_names[1] = "state";
|
||||
input_names[2] = "sr";
|
||||
inputs[0] = in_input;
|
||||
inputs[1] = in_state;
|
||||
inputs[2] = in_sr;
|
||||
|
||||
output_names[0] = "output";
|
||||
output_names[1] = "stateN";
|
||||
outputs[0] = out_prob;
|
||||
outputs[1] = out_state;
|
||||
|
||||
status = ort->Run(vad->session, NULL, input_names, inputs, 3, output_names, 2, outputs);
|
||||
if (status) {
|
||||
vad_report_ort_error(ort, status, "Run");
|
||||
goto done;
|
||||
}
|
||||
|
||||
memcpy(vad->state, next_state, sizeof(next_state));
|
||||
memcpy(vad->context, input + SILERO_VAD_INPUT_SAMPLES - SILERO_VAD_CONTEXT_SAMPLES, sizeof(vad->context));
|
||||
*prob = prob_local;
|
||||
rc = 0;
|
||||
|
||||
done:
|
||||
if (in_input) ort->ReleaseValue(in_input);
|
||||
if (in_state) ort->ReleaseValue(in_state);
|
||||
if (in_sr) ort->ReleaseValue(in_sr);
|
||||
if (out_prob) ort->ReleaseValue(out_prob);
|
||||
if (out_state) ort->ReleaseValue(out_state);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int silero_vad_process_pcm16(silero_vad_t* vad, const int16_t* pcm, float* prob) {
|
||||
float samples[SILERO_VAD_WINDOW_SAMPLES];
|
||||
int i;
|
||||
|
||||
if (!pcm) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_process_pcm16: pcm == NULL");
|
||||
return -1;
|
||||
}
|
||||
for (i = 0; i < SILERO_VAD_WINDOW_SAMPLES; i++) {
|
||||
samples[i] = (float)pcm[i] / 32768.0f;
|
||||
}
|
||||
return silero_vad_process(vad, samples, prob);
|
||||
}
|
||||
|
||||
#else /* !HAVE_SILERO_VAD — пустые стабы (сборки без onnxruntime) */
|
||||
|
||||
silero_vad_t* silero_vad_create(const char* model_path) {
|
||||
(void)model_path;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create: built without onnxruntime (HAVE_SILERO_VAD not defined)");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
silero_vad_t* silero_vad_create_default(void) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create_default: built without onnxruntime (HAVE_SILERO_VAD not defined)");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void silero_vad_destroy(silero_vad_t* vad) {
|
||||
(void)vad;
|
||||
}
|
||||
|
||||
void silero_vad_reset(silero_vad_t* vad) {
|
||||
(void)vad;
|
||||
}
|
||||
|
||||
int silero_vad_process(silero_vad_t* vad, const float* samples, float* prob) {
|
||||
(void)vad; (void)samples; (void)prob;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int silero_vad_process_pcm16(silero_vad_t* vad, const int16_t* pcm, float* prob) {
|
||||
(void)vad; (void)pcm; (void)prob;
|
||||
return -1;
|
||||
}
|
||||
|
||||
#endif /* HAVE_SILERO_VAD */
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Silero VAD (Voice Activity Detector) — C-обёртка над ONNX Runtime.
|
||||
*
|
||||
* Запускает официальную стриминговую модель silero_vad.onnx (v5, ~2.3 МБ),
|
||||
* которая по окну аудио возвращает вероятность наличия речи [0..1].
|
||||
* Рекуррентное состояние модели хранится внутри объекта и переносится между
|
||||
* вызовами. Обёртка также хранит 64 последних отсчёта аудио и добавляет их
|
||||
* перед новым окном: ONNX получает 576 отсчётов, публичный API принимает 512.
|
||||
*
|
||||
* Требования к входному аудио:
|
||||
* - частота дискретизации 16 кГц (модель поддерживает только 16k);
|
||||
* - моно;
|
||||
* - окно ровно SILERO_VAD_WINDOW_SAMPLES = 512 сэмплов (32 мс);
|
||||
* - float-сэмплы в диапазоне примерно [-1;1] (для int16 есть
|
||||
* silero_vad_process_pcm16, который сам делит на 32768).
|
||||
*
|
||||
* Порог и гистерезис (задержки начала/конца речи) намеренно оставлены
|
||||
* вызывающему — радио/звонок сами решают, как интерпретировать вероятность.
|
||||
*
|
||||
* Зависимость: libonnxruntime (C API). Сборка включается опцией
|
||||
* `--with-silero-vad` (см. configure.ac) или макросом HAVE_SILERO_VAD в
|
||||
* CMake-сборках. Без HAVE_SILERO_VAD модуль компилируется в пустые стабы
|
||||
* (все create-функции возвращают NULL, process — ошибку), чтобы исходники lib/
|
||||
* можно было GLOB-ить во всех сборках (Android/chatgui) без onnxruntime.
|
||||
*
|
||||
* Лицензия модели: MIT (Silero Team, https://github.com/snakers4/silero-vad).
|
||||
*/
|
||||
#ifndef SILERO_VAD_H
|
||||
#define SILERO_VAD_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Фиксированное окно стриминговой модели: 512 сэмплов @ 16 кГц = 32 мс */
|
||||
#define SILERO_VAD_WINDOW_SAMPLES 512
|
||||
#define SILERO_VAD_SAMPLE_RATE 16000
|
||||
|
||||
typedef struct silero_vad silero_vad_t;
|
||||
|
||||
/**
|
||||
* Создать детектор, загрузив ONNX-модель из файла model_path.
|
||||
* Возвращает NULL при ошибке (подробности — в лог категории "vad").
|
||||
*/
|
||||
silero_vad_t* silero_vad_create(const char* model_path);
|
||||
|
||||
/**
|
||||
* Создать детектор из модели, вшитой в бинарник (silero_vad_model_data в
|
||||
* silero_vad.c). Не требует файла на диске. NULL при ошибке.
|
||||
*/
|
||||
silero_vad_t* silero_vad_create_default(void);
|
||||
|
||||
/* Освободить детектор. NULL безопасен. */
|
||||
void silero_vad_destroy(silero_vad_t* vad);
|
||||
|
||||
/* Сбросить рекуррентное состояние и звуковой контекст (начало нового потока / разрыв захвата). */
|
||||
void silero_vad_reset(silero_vad_t* vad);
|
||||
|
||||
/**
|
||||
* Обработать окно из 512 float-сэмплов @ 16 кГц (примерно [-1;1]).
|
||||
* При успехе возвращает 0 и заполняет *prob вероятностью речи [0..1];
|
||||
* при ошибке — отрицательное значение (проб не трогается).
|
||||
*/
|
||||
int silero_vad_process(silero_vad_t* vad, const float* samples, float* prob);
|
||||
|
||||
/**
|
||||
* То же, но вход — int16 PCM: сэмплы преобразуются к float делением на 32768.
|
||||
*/
|
||||
int silero_vad_process_pcm16(silero_vad_t* vad, const int16_t* pcm, float* prob);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SILERO_VAD_H */
|
||||
Binary file not shown.
+136918
File diff suppressed because it is too large.
Load diff
@@ -246,3 +246,16 @@ uint16_t ss_get_port(const struct sockaddr_storage *addr) {
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t socket_get_bound_port(socket_t sock) {
|
||||
struct sockaddr_storage address; socklen_t size = sizeof(address);
|
||||
if (getsockname(sock, (struct sockaddr*)&address, &size) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "getsockname fd=%llu failed: %s",
|
||||
(unsigned long long)sock, socket_strerror(socket_get_error()));
|
||||
return 0;
|
||||
}
|
||||
uint16_t port = ss_get_port(&address);
|
||||
if (!port) DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "socket fd=%llu family=%d has no bound port",
|
||||
(unsigned long long)sock, address.ss_family);
|
||||
return port;
|
||||
}
|
||||
+21
-8
@@ -1,6 +1,9 @@
|
||||
/**
|
||||
* Socket compatibility layer for cross-platform support (POSIX / Windows)
|
||||
* MSYS2 UCRT64 compatible
|
||||
* Единые socket_t, коды ошибок и wrappers сокетов POSIX/Windows (включая MSYS2 UCRT64).
|
||||
* platform_init/cleanup обслуживают Winsock; UDP/nonblocking/options/sendto/recvfrom — операции ОС.
|
||||
* Созданные сокеты закрывает владелец через socket_close_wrapper; event loop находится в u_async.h.
|
||||
* Использование: platform_init → create_udp → set_nonblocking/options → sendto/recvfrom → close → cleanup.
|
||||
* До регистрации в uasync вызывающий должен включить nonblocking. Winsock init/cleanup согласовать между потоками.
|
||||
*/
|
||||
|
||||
#ifndef SOCKET_COMPAT_H
|
||||
@@ -44,13 +47,16 @@ extern "C" {
|
||||
#define ERR_INTR EINTR
|
||||
#endif
|
||||
|
||||
// Platform initialization/cleanup
|
||||
// POSIX: 0 без системной инициализации. Windows: WSAStartup при первом init, счётчик вложенности; 0 / -1.
|
||||
int socket_platform_init(void);
|
||||
// Парный cleanup: Windows уменьшает счётчик и вызывает WSACleanup на нуле; POSIX ничего не освобождает.
|
||||
void socket_platform_cleanup(void);
|
||||
|
||||
// Socket operations
|
||||
// UDP-сокет для family (обычно AF_INET/AF_INET6); SOCKET_INVALID при ошибке. Владение передаётся вызывающему.
|
||||
socket_t socket_create_udp(int family);
|
||||
// Включает nonblocking: 0 — успех, -1 — ошибка.
|
||||
int socket_set_nonblocking(socket_t sock);
|
||||
/* Закрыть сокет средствами ОС; на POSIX sock=0 оставляет дескриптор открытым и возвращает 0. */
|
||||
int socket_close_wrapper(socket_t sock);
|
||||
static inline int socket_get_error(void) {
|
||||
#ifdef _WIN32
|
||||
@@ -60,23 +66,30 @@ static inline int socket_get_error(void) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Socket options
|
||||
// Пробует установить оба буфера SO_SNDBUF/SO_RCVBUF: 0 при успехе обоих, -1 при любой ошибке; отката нет.
|
||||
int socket_set_buffers(socket_t sock, int sndbuf, int rcvbuf);
|
||||
// Устанавливает SO_REUSEADDR в значение reuse; 0 / -1.
|
||||
int socket_set_reuseaddr(socket_t sock, int reuse);
|
||||
// SO_BINDTODEVICE для строки ifname (не NULL); 0 / -1. Без поддержки опции, в том числе Windows, возвращает -1.
|
||||
int socket_bind_to_device(socket_t sock, const char* ifname);
|
||||
// SO_MARK в значение mark; 0 / -1. Без поддержки опции, в том числе Windows, возвращает -1.
|
||||
int socket_set_mark(socket_t sock, int mark);
|
||||
|
||||
// I/O operations
|
||||
// Один sendto/recvfrom с flags=0: число байт или SOCKET_ERROR_CODE; errno/WSA-код читать через socket_get_error.
|
||||
// буферами владеет вызывающий; для Windows len должен помещаться в int. dest_len — размер адреса назначения;
|
||||
// src_len при приёме — входная ёмкость/выходная длина адреса (может быть NULL по правилам recvfrom ОС).
|
||||
ssize_t socket_sendto(socket_t sock, const void* buf, size_t len,
|
||||
const struct sockaddr* dest, socklen_t dest_len);
|
||||
ssize_t socket_recvfrom(socket_t sock, void* buf, size_t len,
|
||||
struct sockaddr* src, socklen_t* src_len);
|
||||
|
||||
// Utility
|
||||
// Заимствованный текст ошибки: Windows — один статический буфер (перезаписывается следующим вызовом), POSIX — strerror.
|
||||
const char* socket_strerror(int err);
|
||||
|
||||
// взять порт из sockaddr_storage
|
||||
// IPv4/IPv6-порт в host byte order; 0 при NULL или другом семействе.
|
||||
uint16_t ss_get_port(const struct sockaddr_storage *addr);
|
||||
// Фактический IPv4/IPv6-порт уже привязанного сокета; 0 при ошибке.
|
||||
uint16_t socket_get_bound_port(socket_t sock);
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
# socket_compat — Cross-platform socket abstraction
|
||||
|
||||
## 1. Назначение
|
||||
Унифицирует работу с UDP-сокетами между POSIX и Windows (MSYS2 UCRT64). Скрывает различия в типах дескрипторов (`int` vs `SOCKET`), инициализации подсистемы (WSAStartup/WSACleanup), кодах ошибок и сигнатурах системных вызовов.
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
### Инициализация (однократно при старте)
|
||||
```c
|
||||
if (socket_platform_init() != 0) { /* фатальная ошибка */ }
|
||||
// ... работа с сокетами ...
|
||||
socket_platform_cleanup(); // при завершении
|
||||
```
|
||||
На Windows WSAStartup/WSACleanup с refcount — можно вызывать init/cleanup вложенно.
|
||||
|
||||
### Создание и настройка сокета
|
||||
```c
|
||||
socket_t sock = socket_create_udp(AF_INET); // или AF_INET6
|
||||
if (sock == SOCKET_INVALID) { /* ошибка */ }
|
||||
|
||||
socket_set_nonblocking(sock); // обязательно для u_async
|
||||
socket_set_reuseaddr(sock, 1);
|
||||
socket_set_buffers(sock, 256*1024, 256*1024); // SO_SNDBUF/SO_RCVBUF
|
||||
socket_bind_to_device(sock, "eth0"); // только Linux, SO_BINDTODEVICE
|
||||
socket_set_mark(sock, 42); // только Linux, SO_MARK
|
||||
```
|
||||
|
||||
### Отправка и приём
|
||||
```c
|
||||
ssize_t sent = socket_sendto(sock, buf, len, (struct sockaddr*)&addr, addr_len);
|
||||
ssize_t recv = socket_recvfrom(sock, buf, sizeof(buf), (struct sockaddr*)&src, &src_len);
|
||||
|
||||
uint16_t port = ss_get_port(&src); // порт из sockaddr_storage
|
||||
```
|
||||
|
||||
### Обработка ошибок
|
||||
```c
|
||||
if (sent == SOCKET_ERROR_CODE) {
|
||||
int err = socket_get_error();
|
||||
if (err == ERR_WOULDBLOCK) { /* нормально для неблокирующего */ }
|
||||
// на POSIX ERR_WOULDBLOCK == EWOULDBLOCK (может совпадать с EAGAIN)
|
||||
DEBUG_ERROR(..., "%s", socket_strerror(err));
|
||||
}
|
||||
```
|
||||
|
||||
### Закрытие
|
||||
```c
|
||||
socket_close_wrapper(sock); // на POSIX: close() с защитой от fd 0
|
||||
```
|
||||
|
||||
### Ключевые нюансы
|
||||
- **Всегда вызывать `socket_platform_init()` перед работой** — иначе на Windows сокеты не будут работать
|
||||
- **Всегда `socket_set_nonblocking()`** — u_async требует неблокирующих сокетов
|
||||
- `socket_t` — на POSIX `int`, на Windows `SOCKET` (unsigned)
|
||||
- `SOCKET_INVALID` — константа для невалидного сокета (`-1` на POSIX, `INVALID_SOCKET` на Windows)
|
||||
- `SOCKET_ERROR_CODE` — код ошибки сокетных вызовов (`-1` на POSIX, `SOCKET_ERROR` на Windows)
|
||||
- `socket_strerror()` на Windows использует статический буфер — не thread-safe для параллельного использования
|
||||
- `socket_bind_to_device` и `socket_set_mark` — только Linux, на остальных платформах возвращают -1 с DEBUG-сообщением
|
||||
- `socket_close_wrapper` на POSIX не закроет fd 0 (защита)
|
||||
|
||||
## 3. API
|
||||
|
||||
### Типы и константы
|
||||
| Имя | Назначение |
|
||||
|-----|------------|
|
||||
| `socket_t` | Кроссплатформенный тип дескриптора сокета (`int` / `SOCKET`) |
|
||||
| `SOCKET_INVALID` | Невалидный сокет (`-1` / `INVALID_SOCKET`) |
|
||||
| `SOCKET_ERROR_CODE` | Код ошибки сокетного вызова (`-1` / `SOCKET_ERROR`) |
|
||||
| `ERR_WOULDBLOCK` / `ERR_AGAIN` / `ERR_INTR` | Кроссплатформенные коды ошибок |
|
||||
|
||||
### Инициализация
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `socket_platform_init()` | Инициализация сокетной подсистемы (WSAStartup на Windows). 0 — успех |
|
||||
| `socket_platform_cleanup()` | Деинициализация (WSACleanup на Windows), refcount |
|
||||
|
||||
### Создание и настройка
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `socket_create_udp(family)` | Создать UDP-сокет (AF_INET или AF_INET6) |
|
||||
| `socket_set_nonblocking(sock)` | Перевести в неблокирующий режим |
|
||||
| `socket_set_buffers(sock, snd, rcv)` | Установить SO_SNDBUF и SO_RCVBUF |
|
||||
| `socket_set_reuseaddr(sock, reuse)` | Установить SO_REUSEADDR |
|
||||
| `socket_bind_to_device(sock, ifname)` | Привязать к интерфейсу (Linux SO_BINDTODEVICE) |
|
||||
| `socket_set_mark(sock, mark)` | Установить SO_MARK (Linux) |
|
||||
|
||||
### I/O
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `socket_sendto(sock, buf, len, dest, dest_len)` | Отправить UDP-датаграмму |
|
||||
| `socket_recvfrom(sock, buf, len, src, src_len)` | Принять UDP-датаграмму |
|
||||
|
||||
### Утилиты
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `socket_get_error()` | Текущий код ошибки (inline: `WSAGetLastError()` / `errno`) |
|
||||
| `socket_strerror(err)` | Текстовое описание ошибки (Windows: статический буфер, не thread-safe) |
|
||||
| `socket_close_wrapper(sock)` | Закрыть сокет (POSIX: защита от закрытия fd 0) |
|
||||
| `ss_get_port(addr)` | Извлечь порт в host byte order из `sockaddr_storage` |
|
||||
@@ -67,6 +67,7 @@
|
||||
|
||||
#include "FIFOSamplePipe.h"
|
||||
#include "STTypes.h"
|
||||
#include "SourceSpan.h"
|
||||
|
||||
namespace soundtouch
|
||||
{
|
||||
@@ -197,6 +198,8 @@ private:
|
||||
/// 'virtualPitch' parameters.
|
||||
void calcEffectiveRateAndTempo();
|
||||
|
||||
bool sourceProfileSupported() const;
|
||||
|
||||
protected :
|
||||
/// Number of channels
|
||||
uint channels;
|
||||
@@ -211,6 +214,13 @@ public:
|
||||
SoundTouch();
|
||||
~SoundTouch() override;
|
||||
|
||||
/* uTun: подключать после настройки, до первого putSamples либо после clear.
|
||||
* Поддерживается float PCM, unity pitch/rate, cubic interpolation, AA off, mono/stereo.
|
||||
* Ошибка сообщает Invalid и возвращает false. Изменение профиля после начала и flush
|
||||
* инвалидируют карту; clear начинает новую. nullptr отключает события в любой момент.
|
||||
* События описывают произведённый выход, включая ещё не прочитанные receiveSamples данные. */
|
||||
bool setSourceCallback(SourceCallback callback, void* context);
|
||||
|
||||
/// Get SoundTouch library version string
|
||||
static const char *getVersionString();
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/* uTun: происхождение PCM без изменения DSP. Индексы — сэмплы НА КАНАЛ от clear().
|
||||
* Copy: out[i] происходит из input0+i. Mix: источники input0+i и input1+i,
|
||||
* вес второго i/count, первого 1-i/count. Это описание происхождения, не PCM.
|
||||
* Reset начинает новую шкалу. Invalid запрещает её использование до clear().
|
||||
* Callback синхронный: нельзя вызывать SoundTouch повторно, блокировать поток или
|
||||
* сохранять ссылку на событие. Ошибки передаются через Invalid; логирует потребитель. */
|
||||
#ifndef SOUNDTOUCH_SOURCE_SPAN_H
|
||||
#define SOUNDTOUCH_SOURCE_SPAN_H
|
||||
#include <cstdint>
|
||||
|
||||
namespace soundtouch {
|
||||
enum class SourceKind { Reset, Copy, Mix, Invalid };
|
||||
enum class SourceError { None, Profile, Reconfigure, Flush, LateAttach };
|
||||
struct SourceSpan {
|
||||
SourceKind kind;
|
||||
SourceError error;
|
||||
uint64_t output, input0, input1;
|
||||
uint32_t count;
|
||||
SourceSpan(SourceKind k, SourceError e = SourceError::None, uint64_t out = 0,
|
||||
uint64_t first = 0, uint64_t second = 0, uint32_t length = 0)
|
||||
: kind(k), error(e), output(out), input0(first), input1(second), count(length) {}
|
||||
};
|
||||
using SourceCallback = void (*)(void*, const SourceSpan&);
|
||||
}
|
||||
#endif
|
||||
@@ -58,6 +58,9 @@ public:
|
||||
|
||||
virtual void resetRegisters() override;
|
||||
|
||||
// При fract=0 кубический интерполятор копирует psrc[1], сохраняя четыре входных сэмпла.
|
||||
int unitySourceOffset() const override { return rate == 1.0 && fract == 0.0 ? 1 : -1; }
|
||||
|
||||
virtual int getLatency() const override
|
||||
{
|
||||
return 1;
|
||||
|
||||
@@ -83,6 +83,9 @@ public:
|
||||
virtual void setChannels(int channels);
|
||||
virtual int getLatency() const = 0;
|
||||
|
||||
// uTun: смещение точного копирования при rate=1; -1 если реализация не поддерживает карту.
|
||||
virtual int unitySourceOffset() const { return -1; }
|
||||
|
||||
virtual void resetRegisters() = 0;
|
||||
|
||||
// static factory function
|
||||
@@ -133,6 +136,9 @@ public:
|
||||
AAFilter *getAAFilter();
|
||||
|
||||
/// Enables/disables the anti-alias filter. Zero to disable, nonzero to enable
|
||||
// clear() добавляет getLatency() нулей: они компенсируют смещение интерполятора.
|
||||
int unitySourceOffset() const { return bUseAAFilter ? -1 : pTransposer->unitySourceOffset() - getLatency(); }
|
||||
|
||||
void enableAAFilter(bool newMode);
|
||||
|
||||
/// Returns nonzero if anti-alias filter is enabled.
|
||||
|
||||
@@ -117,6 +117,28 @@ SoundTouch::~SoundTouch()
|
||||
delete pTDStretch;
|
||||
}
|
||||
|
||||
bool SoundTouch::sourceProfileSupported() const
|
||||
{
|
||||
#if defined(SOUNDTOUCH_FLOAT_SAMPLES) && !defined(SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER)
|
||||
return rate == 1.0 && virtualPitch == 1.0 && channels >= 1 && channels <= 2
|
||||
&& bSrateSet && pRateTransposer->unitySourceOffset() >= 0;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SoundTouch::setSourceCallback(SourceCallback callback, void* context)
|
||||
{
|
||||
if (!callback) { pTDStretch->setSourceCallback(nullptr, nullptr, 0); return true; }
|
||||
if (samplesExpectedOut != 0 || !sourceProfileSupported())
|
||||
{
|
||||
callback(context, {SourceKind::Invalid, samplesExpectedOut != 0 ? SourceError::LateAttach : SourceError::Profile});
|
||||
return false;
|
||||
}
|
||||
pTDStretch->setSourceCallback(callback, context, (unsigned)pRateTransposer->unitySourceOffset());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/// Get SoundTouch library version string
|
||||
const char *SoundTouch::getVersionString()
|
||||
@@ -139,6 +161,8 @@ void SoundTouch::setChannels(uint numChannels)
|
||||
{
|
||||
if (!verifyNumberOfChannels(numChannels)) return;
|
||||
|
||||
if (samplesExpectedOut != 0) pTDStretch->invalidateSource(SourceError::Reconfigure);
|
||||
|
||||
channels = numChannels;
|
||||
pRateTransposer->setChannels((int)numChannels);
|
||||
pTDStretch->setChannels((int)numChannels);
|
||||
@@ -223,6 +247,8 @@ void SoundTouch::calcEffectiveRateAndTempo()
|
||||
tempo = virtualTempo / virtualPitch;
|
||||
rate = virtualPitch * virtualRate;
|
||||
|
||||
if (rate != 1.0 || virtualPitch != 1.0) pTDStretch->invalidateSource(SourceError::Profile);
|
||||
|
||||
if (!TEST_FLOAT_EQUAL(rate,oldRate)) pRateTransposer->setRate(rate);
|
||||
if (!TEST_FLOAT_EQUAL(tempo, oldTempo)) pTDStretch->setTempo(tempo);
|
||||
|
||||
@@ -266,6 +292,7 @@ void SoundTouch::calcEffectiveRateAndTempo()
|
||||
// Sets sample rate.
|
||||
void SoundTouch::setSampleRate(uint srate)
|
||||
{
|
||||
if (samplesExpectedOut != 0) pTDStretch->invalidateSource(SourceError::Reconfigure);
|
||||
// set sample rate, leave other tempo changer parameters as they are.
|
||||
pTDStretch->setParameters((int)srate);
|
||||
bSrateSet = true;
|
||||
@@ -285,6 +312,8 @@ void SoundTouch::putSamples(const SAMPLETYPE *samples, uint nSamples)
|
||||
ST_THROW_RT_ERROR("SoundTouch : Number of channels not defined");
|
||||
}
|
||||
|
||||
if (!sourceProfileSupported()) pTDStretch->invalidateSource(SourceError::Profile);
|
||||
|
||||
// accumulate how many samples are expected out from processing, given the current
|
||||
// processing setting
|
||||
samplesExpectedOut += (double)nSamples / ((double)rate * (double)tempo);
|
||||
@@ -317,6 +346,8 @@ void SoundTouch::putSamples(const SAMPLETYPE *samples, uint nSamples)
|
||||
// in the middle of a sound stream.
|
||||
void SoundTouch::flush()
|
||||
{
|
||||
// Padding и обрезка финального выхода не входят в контракт live-карты.
|
||||
pTDStretch->invalidateSource(SourceError::Flush);
|
||||
int i;
|
||||
int numStillExpected;
|
||||
SAMPLETYPE *buff = new SAMPLETYPE[128 * channels];
|
||||
@@ -350,6 +381,8 @@ void SoundTouch::flush()
|
||||
// 'SETTING_...' defines for available setting ID's.
|
||||
bool SoundTouch::setSetting(int settingId, int value)
|
||||
{
|
||||
if (samplesExpectedOut != 0 && settingId != SETTING_USE_QUICKSEEK)
|
||||
pTDStretch->invalidateSource(SourceError::Reconfigure);
|
||||
int sampleRate, sequenceMs, seekWindowMs, overlapMs;
|
||||
|
||||
// read current tdstretch routine parameters
|
||||
|
||||
@@ -210,6 +210,35 @@ void TDStretch::clear()
|
||||
{
|
||||
outputBuffer.clear();
|
||||
clearInput();
|
||||
sourceInput = sourceOutput = sourceMid = 0;
|
||||
sourceValid = sourceCallback != nullptr;
|
||||
if (sourceCallback) sourceCallback(sourceContext, {SourceKind::Reset});
|
||||
}
|
||||
|
||||
// uTun: метаданные принадлежат потребителю, SoundTouch не выделяет память для карты.
|
||||
void TDStretch::setSourceCallback(SourceCallback callback, void* context, unsigned offset)
|
||||
{
|
||||
sourceCallback = callback;
|
||||
sourceContext = context;
|
||||
sourceOffset = offset;
|
||||
sourceInput = sourceOutput = sourceMid = 0;
|
||||
sourceValid = callback != nullptr;
|
||||
if (callback) callback(context, {SourceKind::Reset});
|
||||
}
|
||||
|
||||
void TDStretch::invalidateSource(SourceError reason)
|
||||
{
|
||||
if (!sourceValid) return;
|
||||
sourceValid = false;
|
||||
sourceCallback(sourceContext, {SourceKind::Invalid, reason});
|
||||
}
|
||||
|
||||
void TDStretch::emitSource(SourceKind kind, uint64_t first, uint64_t second, unsigned count)
|
||||
{
|
||||
if (!sourceValid || !count) return;
|
||||
sourceCallback(sourceContext, {kind, SourceError::None, sourceOutput,
|
||||
first + sourceOffset, second + sourceOffset, count});
|
||||
sourceOutput += count;
|
||||
}
|
||||
|
||||
|
||||
@@ -645,6 +674,7 @@ void TDStretch::processSamples()
|
||||
// (that's in 'midBuffer')
|
||||
overlap(outputBuffer.ptrEnd((uint)overlapLength), inputBuffer.ptrBegin(), (uint)offset);
|
||||
outputBuffer.putSamples((uint)overlapLength);
|
||||
emitSource(SourceKind::Mix, sourceMid, sourceInput + offset, (unsigned)overlapLength);
|
||||
offset += overlapLength;
|
||||
}
|
||||
else
|
||||
@@ -683,6 +713,7 @@ void TDStretch::processSamples()
|
||||
// length of sequence
|
||||
temp = (seekWindowLength - 2 * overlapLength);
|
||||
outputBuffer.putSamples(inputBuffer.ptrBegin() + channels * offset, (uint)temp);
|
||||
emitSource(SourceKind::Copy, sourceInput + offset, 0, (unsigned)temp);
|
||||
|
||||
// Copies the end of the current sequence from 'inputBuffer' to
|
||||
// 'midBuffer' for being mixed with the beginning of the next
|
||||
@@ -690,6 +721,7 @@ void TDStretch::processSamples()
|
||||
assert((offset + temp + overlapLength) <= (int)inputBuffer.numSamples());
|
||||
memcpy(pMidBuffer, inputBuffer.ptrBegin() + channels * (offset + temp),
|
||||
channels * sizeof(SAMPLETYPE) * overlapLength);
|
||||
sourceMid = sourceInput + offset + temp;
|
||||
|
||||
// Remove the processed samples from the input buffer. Update
|
||||
// the difference between integer & nominal skip step to 'skipFract'
|
||||
@@ -697,7 +729,7 @@ void TDStretch::processSamples()
|
||||
skipFract += nominalSkip; // real skip size
|
||||
ovlSkip = (int)skipFract; // rounded to integer skip
|
||||
skipFract -= ovlSkip; // maintain the fraction part, i.e. real vs. integer skip
|
||||
inputBuffer.receiveSamples((uint)ovlSkip);
|
||||
sourceInput += inputBuffer.receiveSamples((uint)ovlSkip);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include "STTypes.h"
|
||||
#include "RateTransposer.h"
|
||||
#include "FIFOSamplePipe.h"
|
||||
#include "SourceSpan.h"
|
||||
|
||||
namespace soundtouch
|
||||
{
|
||||
@@ -135,6 +136,13 @@ protected:
|
||||
FIFOSampleBuffer outputBuffer;
|
||||
FIFOSampleBuffer inputBuffer;
|
||||
|
||||
SourceCallback sourceCallback = nullptr;
|
||||
void* sourceContext = nullptr;
|
||||
bool sourceValid = false;
|
||||
uint64_t sourceInput = 0, sourceOutput = 0, sourceMid = 0;
|
||||
unsigned sourceOffset = 0;
|
||||
void emitSource(SourceKind kind, uint64_t first, uint64_t second, unsigned count);
|
||||
|
||||
void acceptNewOverlapLength(int newOverlapLength);
|
||||
|
||||
virtual void clearCrossCorrState();
|
||||
@@ -167,6 +175,10 @@ public:
|
||||
TDStretch();
|
||||
virtual ~TDStretch() override;
|
||||
|
||||
// Только SoundTouch: регистрация на пустом тракте, затем события из processSamples().
|
||||
void setSourceCallback(SourceCallback callback, void* context, unsigned offset);
|
||||
void invalidateSource(SourceError reason);
|
||||
|
||||
/// Operator 'new' is overloaded so that it automatically creates a suitable instance
|
||||
/// depending on if we've a MMX/SSE/etc-capable CPU available or not.
|
||||
static void *operator new(size_t s);
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
// speex_aec.c — C-обёртка над SpeexDSP mdf.c (акустическое эхоподавление).
|
||||
//
|
||||
// API см. в speex_aec.h. Ключевые детали реализации:
|
||||
// - собственная линия задержки рендера на depth кадров (SpeexDSP даёт только
|
||||
// фиксированные 2 кадра — PLAYBACK_DELAY в mdf.c);
|
||||
// - синхронный speex_echo_cancellation(rec, play_delayed, out) с выравниванием
|
||||
// рендер↔захват по линии задержки;
|
||||
// - дрейф независимых потоков: переполнение линии → отбрасываем старый кадр,
|
||||
// недозаполнение (старт/underflow) → passthrough без канселлера.
|
||||
//
|
||||
// Рендер принимает чанки любого размера; захват — ровно один полный кадр.
|
||||
// Частые аномалии учитываются счётчиками; владелец публикует сводку вне audio callback.
|
||||
|
||||
#include "speex_aec.h"
|
||||
#include "debug_config.h"
|
||||
#include "mem.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <limits.h>
|
||||
|
||||
#include <speex/speex_echo.h>
|
||||
|
||||
#define AEC_ID "speex_aec"
|
||||
|
||||
struct speex_aec {
|
||||
SpeexEchoState* st;
|
||||
int mic_channels, render_channels;
|
||||
int frame_samples; /* сэмплов в кадре (960 @48кГц) */
|
||||
int depth; /* глубина линии задержки в кадрах (>=1) */
|
||||
int16_t* line; /* кольцо depth*frame_samples */
|
||||
int head; /* индекс самого старого кадра в line */
|
||||
int count; /* кадров в линии (0..depth) */
|
||||
int16_t* play_acc; /* аккумулятор рендера до кадра */
|
||||
int play_len;
|
||||
uint32_t overruns; /* отброшено рендер-кадров (дрейф: рендер быстрее) */
|
||||
uint32_t underruns; /* passthrough-кадров (дрейф: рендер медленнее/старт) */
|
||||
};
|
||||
|
||||
/* Положить полный рендер-кадр в линию задержки (вытесняя старейший при переполнении). */
|
||||
static void aec_enqueue(speex_aec_t* a, const int16_t* frame) {
|
||||
if (a->count == a->depth) {
|
||||
a->head = (a->head + 1) % a->depth;
|
||||
a->count--;
|
||||
a->overruns++;
|
||||
}
|
||||
memcpy(a->line + (size_t)((a->head + a->count) % a->depth) * a->frame_samples * a->render_channels,
|
||||
frame, (size_t)a->frame_samples * a->render_channels * sizeof(int16_t));
|
||||
a->count++;
|
||||
}
|
||||
|
||||
speex_aec_t* speex_aec_create(int sample_rate, int frame_samples, int filter_samples, int delay_frames) {
|
||||
return speex_aec_create_mc(sample_rate, frame_samples, filter_samples, delay_frames, 1, 1);
|
||||
}
|
||||
|
||||
/* Независимые каналы референса: противофазное стерео не усредняется. */
|
||||
speex_aec_t* speex_aec_create_mc(int sample_rate, int frame_samples, int filter_samples, int delay_frames,
|
||||
int mic_channels, int render_channels) {
|
||||
speex_aec_t* a;
|
||||
int rate;
|
||||
|
||||
if (sample_rate <= 0 || frame_samples <= 0 || filter_samples < frame_samples ||
|
||||
mic_channels < 1 || mic_channels > 2 || render_channels < 1 || render_channels > 2 ||
|
||||
frame_samples > INT_MAX / render_channels || frame_samples > INT_MAX / mic_channels ||
|
||||
(delay_frames > 0 && delay_frames > INT_MAX / frame_samples / render_channels)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: bad args rate=%d frame=%d filter=%d delay=%d",
|
||||
AEC_ID, sample_rate, frame_samples, filter_samples, delay_frames);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
a = (speex_aec_t*)u_calloc(1, sizeof(*a));
|
||||
if (!a) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: OOM for state", AEC_ID);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
a->st = speex_echo_state_init_mc(frame_samples, filter_samples, mic_channels, render_channels);
|
||||
if (!a->st) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: echo state allocation failed", AEC_ID);
|
||||
u_free(a);
|
||||
return NULL;
|
||||
}
|
||||
rate = sample_rate;
|
||||
if (speex_echo_ctl(a->st, SPEEX_ECHO_SET_SAMPLING_RATE, &rate) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: SET_SAMPLING_RATE(%d) failed", AEC_ID, sample_rate);
|
||||
speex_echo_state_destroy(a->st);
|
||||
u_free(a);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
a->mic_channels = mic_channels; a->render_channels = render_channels;
|
||||
a->frame_samples = frame_samples;
|
||||
a->depth = delay_frames > 0 ? delay_frames : 1;
|
||||
a->line = (int16_t*)u_calloc((uint32_t)(a->depth * frame_samples * render_channels), sizeof(int16_t));
|
||||
a->play_acc = (int16_t*)u_calloc((uint32_t)(frame_samples * render_channels), sizeof(int16_t));
|
||||
if (!a->line || !a->play_acc) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: OOM for buffers", AEC_ID);
|
||||
if (a->line) u_free(a->line);
|
||||
if (a->play_acc) u_free(a->play_acc);
|
||||
speex_echo_state_destroy(a->st);
|
||||
u_free(a);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_AEC, "%s: created rate=%d frame=%d filter=%d delay=%d frames mic=%d render=%d",
|
||||
AEC_ID, sample_rate, frame_samples, filter_samples, a->depth, mic_channels, render_channels);
|
||||
return a;
|
||||
}
|
||||
|
||||
void speex_aec_destroy(speex_aec_t* a) {
|
||||
if (!a) return;
|
||||
speex_echo_state_destroy(a->st);
|
||||
u_free(a->line);
|
||||
u_free(a->play_acc);
|
||||
u_free(a);
|
||||
}
|
||||
|
||||
void speex_aec_reset(speex_aec_t* a) {
|
||||
if (!a) return;
|
||||
speex_echo_state_reset(a->st);
|
||||
a->head = 0;
|
||||
a->count = 0;
|
||||
a->play_len = 0;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_AEC, "%s: reset (filter + delay line)", AEC_ID);
|
||||
}
|
||||
|
||||
void speex_aec_feed_playback(speex_aec_t* a, const int16_t* pcm, int count) {
|
||||
int fs, take;
|
||||
|
||||
if (!a || !pcm || count <= 0) return;
|
||||
fs = a->frame_samples * a->render_channels;
|
||||
|
||||
while (count > 0) {
|
||||
take = fs - a->play_len;
|
||||
if (take > count) take = count;
|
||||
memcpy(a->play_acc + a->play_len, pcm, (size_t)take * sizeof(int16_t));
|
||||
a->play_len += take;
|
||||
pcm += take;
|
||||
count -= take;
|
||||
if (a->play_len == fs) {
|
||||
aec_enqueue(a, a->play_acc);
|
||||
a->play_len = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int speex_aec_process_capture(speex_aec_t* a, const int16_t* pcm, int count, int16_t* out) {
|
||||
int fs;
|
||||
|
||||
if (!a || !pcm || !out || count <= 0) return 0;
|
||||
fs = a->frame_samples;
|
||||
|
||||
if (count != fs * a->mic_channels) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: capture requires one frame samples=%d expected=%d", AEC_ID, count, fs * a->mic_channels);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (a->count == a->depth) {
|
||||
const int16_t* ref = a->line + (size_t)a->head * fs * a->render_channels;
|
||||
speex_echo_cancellation(a->st, pcm, ref, out);
|
||||
a->head = (a->head + 1) % a->depth;
|
||||
a->count--;
|
||||
} else {
|
||||
/* линия не наполнена (старт или рендер отстаёт): passthrough без канселлера */
|
||||
memcpy(out, pcm, (size_t)count * sizeof(int16_t));
|
||||
a->underruns++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
int speex_aec_delay_fill(const speex_aec_t* a) {
|
||||
return a ? a->count : 0;
|
||||
}
|
||||
|
||||
void speex_aec_get_stats(const speex_aec_t* a, uint32_t* overruns, uint32_t* underruns) {
|
||||
*overruns = a ? a->overruns : 0;
|
||||
*underruns = a ? a->underruns : 0;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* speex_aec.h — акустическое эхоподавление (AEC), C-обёртка над SpeexDSP mdf.c.
|
||||
*
|
||||
* Подавляет эхо дальнего конца (то, что играем в динамик) в сигнале микрофона
|
||||
* перед кодированием. Работает на interleaved int16 PCM, 1/2 канала, частота 48000 (можно
|
||||
* 8000/16000/32000/48000), кадр 20 мс (960 сэмплов @48 кГц).
|
||||
*
|
||||
* Объект не имеет внутренних блокировок: feed/process/reset/get_stats выполняет один владелец
|
||||
* либо вызывающий сериализует их общей блокировкой. Аппаратные callbacks сами AEC не вызывают.
|
||||
*
|
||||
* Модель использования (duplex-контур звонка):
|
||||
* - рендер (far-end, то что пошло в динамик) → speex_aec_feed_playback();
|
||||
* - захват (near-end, микрофон) → speex_aec_process_capture().
|
||||
*
|
||||
* Обёртка держит собственную линию задержки рендера на `delay_frames` кадров и
|
||||
* зовёт синхронный speex_echo_cancellation(rec, play_delayed, out). Это делает
|
||||
* выравнивание рендер↔захват предсказуемым и настраиваемым (встроенный буфер
|
||||
* SpeexDSP фиксирован в 2 кадра и для Android-задержки не годится).
|
||||
*
|
||||
* Дрейф двух независимых потоков (capture/play на Android) компенсируется
|
||||
* ограниченной глубиной линии: переполнение → отбрасываем старый рендер-кадр,
|
||||
* недозаполнение → passthrough без канселлера; счётчики публикует владелец вне callback.
|
||||
*
|
||||
* Зависимости: SpeexDSP (mdf.c/fftwrap.c/kiss_fft*) из lib/speexdsp, флаги
|
||||
* FLOATING_POINT + USE_KISS_FFT. Собирается всегда (без внешних зависимостей).
|
||||
*
|
||||
* Лицензия SpeexDSP: 3-clause BSD (Xiph) — см. lib/speexdsp/COPYING.
|
||||
*/
|
||||
#ifndef SPEEX_AEC_H
|
||||
#define SPEEX_AEC_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct speex_aec speex_aec_t;
|
||||
|
||||
/**
|
||||
* Создать эхоканселлер.
|
||||
* sample_rate — 48000 (также 8000/16000/32000);
|
||||
* frame_samples — сэмплов в кадре (960 = 20 мс @48 кГц);
|
||||
* filter_samples— длина эхо-хвоста в сэмплах (14400 = 300 мс, кратно кадру);
|
||||
* delay_frames — глубина линии рендер→захват в кадрах (значения <=0 приводятся к 1).
|
||||
* Возвращает NULL при ошибке (лог категории "aec").
|
||||
*/
|
||||
speex_aec_t* speex_aec_create(int sample_rate, int frame_samples, int filter_samples, int delay_frames);
|
||||
|
||||
/* Многоканальный AEC: frame_samples на канал, count в feed/process — все interleaved отсчёты.
|
||||
* mic_channels и render_channels — 1/2; возвращаемый count захвата равен frame_samples*mic_channels. */
|
||||
speex_aec_t* speex_aec_create_mc(int sample_rate, int frame_samples, int filter_samples, int delay_frames,
|
||||
int mic_channels, int render_channels);
|
||||
|
||||
/* Освободить канселлер. NULL безопасен. */
|
||||
void speex_aec_destroy(speex_aec_t* aec);
|
||||
|
||||
/* Сбросить адаптивный фильтр и линию задержки (смена устройства/роута). */
|
||||
void speex_aec_reset(speex_aec_t* aec);
|
||||
|
||||
/**
|
||||
* Подать рендер (дальний конец). Накопление до кадра внутри; полный кадр
|
||||
* кладётся в линию задержки. count может быть любым (обычно 480 или 960).
|
||||
*/
|
||||
void speex_aec_feed_playback(speex_aec_t* aec, const int16_t* pcm, int count);
|
||||
|
||||
/**
|
||||
* Обработать ровно один кадр захвата (count == frame_samples*mic_channels).
|
||||
* Возвращает count при успехе, 0 при ошибке. out вмещает полный interleaved кадр
|
||||
* и не должен алиаситься с pcm. Накопитель аппаратных чанков принадлежит I/O-движку.
|
||||
*/
|
||||
int speex_aec_process_capture(speex_aec_t* aec, const int16_t* pcm, int count, int16_t* out);
|
||||
|
||||
/* Актуальная глубина линии задержки (кадров) — для диагностики дрейфа. */
|
||||
int speex_aec_delay_fill(const speex_aec_t* aec);
|
||||
/* Под той же блокировкой, что feed/process; выводить снимок вне callback. */
|
||||
void speex_aec_get_stats(const speex_aec_t* aec, uint32_t* overruns, uint32_t* underruns);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SPEEX_AEC_H */
|
||||
@@ -0,0 +1,35 @@
|
||||
Copyright 2002-2008 Xiph.org Foundation
|
||||
Copyright 2002-2008 Jean-Marc Valin
|
||||
Copyright 2005-2007 Analog Devices Inc.
|
||||
Copyright 2005-2008 Commonwealth Scientific and Industrial Research
|
||||
Organisation (CSIRO)
|
||||
Copyright 1993, 2002, 2006 David Rowe
|
||||
Copyright 2003 EpicGames
|
||||
Copyright 1992-1994 Jutta Degener, Carsten Bormann
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
- Neither the name of the Xiph.org Foundation nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
Copyright (c) 2003-2004, Mark Borgerding
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
* Neither the author nor the names of any contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#define MIN(a,b) ((a)<(b) ? (a):(b))
|
||||
#define MAX(a,b) ((a)>(b) ? (a):(b))
|
||||
|
||||
/* kiss_fft.h
|
||||
defines kiss_fft_scalar as either short or a float type
|
||||
and defines
|
||||
typedef struct { kiss_fft_scalar r; kiss_fft_scalar i; }kiss_fft_cpx; */
|
||||
#include "kiss_fft.h"
|
||||
#include "math_approx.h"
|
||||
|
||||
#define MAXFACTORS 32
|
||||
/* e.g. an fft of length 128 has 4 factors
|
||||
as far as kissfft is concerned
|
||||
4*4*4*2
|
||||
*/
|
||||
|
||||
struct kiss_fft_state{
|
||||
int nfft;
|
||||
int inverse;
|
||||
int factors[2*MAXFACTORS];
|
||||
kiss_fft_cpx twiddles[1];
|
||||
};
|
||||
|
||||
/*
|
||||
Explanation of macros dealing with complex math:
|
||||
|
||||
C_MUL(m,a,b) : m = a*b
|
||||
C_FIXDIV( c , div ) : if a fixed point impl., c /= div. noop otherwise
|
||||
C_SUB( res, a,b) : res = a - b
|
||||
C_SUBFROM( res , a) : res -= a
|
||||
C_ADDTO( res , a) : res += a
|
||||
* */
|
||||
#ifdef FIXED_POINT
|
||||
#include "arch.h"
|
||||
# define FRACBITS 15
|
||||
# define SAMPPROD spx_int32_t
|
||||
#define SAMP_MAX 32767
|
||||
|
||||
#define SAMP_MIN -SAMP_MAX
|
||||
|
||||
#if defined(CHECK_OVERFLOW)
|
||||
# define CHECK_OVERFLOW_OP(a,op,b) \
|
||||
if ( (SAMPPROD)(a) op (SAMPPROD)(b) > SAMP_MAX || (SAMPPROD)(a) op (SAMPPROD)(b) < SAMP_MIN ) { \
|
||||
fprintf(stderr,"WARNING:overflow @ " __FILE__ "(%d): (%d " #op" %d) = %ld\n",__LINE__,(a),(b),(SAMPPROD)(a) op (SAMPPROD)(b) ); }
|
||||
#endif
|
||||
|
||||
|
||||
# define smul(a,b) ( (SAMPPROD)(a)*(b) )
|
||||
# define sround( x ) (kiss_fft_scalar)( ( (x) + (1<<(FRACBITS-1)) ) >> FRACBITS )
|
||||
|
||||
# define S_MUL(a,b) sround( smul(a,b) )
|
||||
|
||||
# define C_MUL(m,a,b) \
|
||||
do{ (m).r = sround( smul((a).r,(b).r) - smul((a).i,(b).i) ); \
|
||||
(m).i = sround( smul((a).r,(b).i) + smul((a).i,(b).r) ); }while(0)
|
||||
|
||||
# define C_MUL4(m,a,b) \
|
||||
do{ (m).r = PSHR32( smul((a).r,(b).r) - smul((a).i,(b).i),17 ); \
|
||||
(m).i = PSHR32( smul((a).r,(b).i) + smul((a).i,(b).r),17 ); }while(0)
|
||||
|
||||
# define DIVSCALAR(x,k) \
|
||||
(x) = sround( smul( x, SAMP_MAX/k ) )
|
||||
|
||||
# define C_FIXDIV(c,div) \
|
||||
do { DIVSCALAR( (c).r , div); \
|
||||
DIVSCALAR( (c).i , div); }while (0)
|
||||
|
||||
# define C_MULBYSCALAR( c, s ) \
|
||||
do{ (c).r = sround( smul( (c).r , s ) ) ;\
|
||||
(c).i = sround( smul( (c).i , s ) ) ; }while(0)
|
||||
|
||||
#else /* not FIXED_POINT*/
|
||||
|
||||
# define S_MUL(a,b) ( (a)*(b) )
|
||||
#define C_MUL(m,a,b) \
|
||||
do{ (m).r = (a).r*(b).r - (a).i*(b).i;\
|
||||
(m).i = (a).r*(b).i + (a).i*(b).r; }while(0)
|
||||
|
||||
#define C_MUL4(m,a,b) C_MUL(m,a,b)
|
||||
|
||||
# define C_FIXDIV(c,div) /* NOOP */
|
||||
# define C_MULBYSCALAR( c, s ) \
|
||||
do{ (c).r *= (s);\
|
||||
(c).i *= (s); }while(0)
|
||||
#endif
|
||||
|
||||
#ifndef CHECK_OVERFLOW_OP
|
||||
# define CHECK_OVERFLOW_OP(a,op,b) /* noop */
|
||||
#endif
|
||||
|
||||
#define C_ADD( res, a,b)\
|
||||
do { \
|
||||
CHECK_OVERFLOW_OP((a).r,+,(b).r)\
|
||||
CHECK_OVERFLOW_OP((a).i,+,(b).i)\
|
||||
(res).r=(a).r+(b).r; (res).i=(a).i+(b).i; \
|
||||
}while(0)
|
||||
#define C_SUB( res, a,b)\
|
||||
do { \
|
||||
CHECK_OVERFLOW_OP((a).r,-,(b).r)\
|
||||
CHECK_OVERFLOW_OP((a).i,-,(b).i)\
|
||||
(res).r=(a).r-(b).r; (res).i=(a).i-(b).i; \
|
||||
}while(0)
|
||||
#define C_ADDTO( res , a)\
|
||||
do { \
|
||||
CHECK_OVERFLOW_OP((res).r,+,(a).r)\
|
||||
CHECK_OVERFLOW_OP((res).i,+,(a).i)\
|
||||
(res).r += (a).r; (res).i += (a).i;\
|
||||
}while(0)
|
||||
|
||||
#define C_SUBFROM( res , a)\
|
||||
do {\
|
||||
CHECK_OVERFLOW_OP((res).r,-,(a).r)\
|
||||
CHECK_OVERFLOW_OP((res).i,-,(a).i)\
|
||||
(res).r -= (a).r; (res).i -= (a).i; \
|
||||
}while(0)
|
||||
|
||||
|
||||
#ifdef FIXED_POINT
|
||||
# define KISS_FFT_COS(phase) floor(MIN(32767,MAX(-32767,.5+32768 * cos (phase))))
|
||||
# define KISS_FFT_SIN(phase) floor(MIN(32767,MAX(-32767,.5+32768 * sin (phase))))
|
||||
# define HALF_OF(x) ((x)>>1)
|
||||
#elif defined(USE_SIMD)
|
||||
# define KISS_FFT_COS(phase) _mm_set1_ps( cos(phase) )
|
||||
# define KISS_FFT_SIN(phase) _mm_set1_ps( sin(phase) )
|
||||
# define HALF_OF(x) ((x)*_mm_set1_ps(.5))
|
||||
#else
|
||||
# define KISS_FFT_COS(phase) (kiss_fft_scalar) cos(phase)
|
||||
# define KISS_FFT_SIN(phase) (kiss_fft_scalar) sin(phase)
|
||||
# define HALF_OF(x) ((x)*.5)
|
||||
#endif
|
||||
|
||||
#define kf_cexp(x,phase) \
|
||||
do{ \
|
||||
(x)->r = KISS_FFT_COS(phase);\
|
||||
(x)->i = KISS_FFT_SIN(phase);\
|
||||
}while(0)
|
||||
#define kf_cexp2(x,phase) \
|
||||
do{ \
|
||||
(x)->r = spx_cos_norm((phase));\
|
||||
(x)->i = spx_cos_norm((phase)-32768);\
|
||||
}while(0)
|
||||
|
||||
|
||||
/* a debugging function */
|
||||
#define pcpx(c)\
|
||||
fprintf(stderr,"%g + %gi\n",(double)((c)->r),(double)((c)->i) )
|
||||
@@ -0,0 +1,232 @@
|
||||
/* Copyright (C) 2003 Jean-Marc Valin */
|
||||
/**
|
||||
@file arch.h
|
||||
@brief Various architecture definitions Speex
|
||||
*/
|
||||
/*
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
- Neither the name of the Xiph.org Foundation nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#ifndef ARCH_H
|
||||
#define ARCH_H
|
||||
|
||||
/* A couple test to catch stupid option combinations */
|
||||
#ifdef FIXED_POINT
|
||||
|
||||
#ifdef FLOATING_POINT
|
||||
#error You cannot compile as floating point and fixed point at the same time
|
||||
#endif
|
||||
#ifdef USE_SSE
|
||||
#error SSE is only for floating-point
|
||||
#endif
|
||||
#if defined(ARM4_ASM) + defined(ARM5E_ASM) + defined(BFIN_ASM) > 1
|
||||
#error Make up your mind. What CPU do you have?
|
||||
#endif
|
||||
#ifdef VORBIS_PSYCHO
|
||||
#error Vorbis-psy model currently not implemented in fixed-point
|
||||
#endif
|
||||
|
||||
#else
|
||||
|
||||
#ifndef FLOATING_POINT
|
||||
#error You now need to define either FIXED_POINT or FLOATING_POINT
|
||||
#endif
|
||||
#if defined(ARM4_ASM) || defined(ARM5E_ASM) || defined(BFIN_ASM)
|
||||
#error I suppose you can have a [ARM4/ARM5E/Blackfin] that has float instructions?
|
||||
#endif
|
||||
#ifdef FIXED_DEBUG
|
||||
#error "Don't you think enabling fixed-point is a good thing to do if you want to debug that?"
|
||||
#endif
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
#ifndef OUTSIDE_SPEEX
|
||||
#include "speex/speexdsp_types.h"
|
||||
#endif
|
||||
|
||||
#define ABS(x) ((x) < 0 ? (-(x)) : (x)) /**< Absolute integer value. */
|
||||
#define ABS16(x) ((x) < 0 ? (-(x)) : (x)) /**< Absolute 16-bit value. */
|
||||
#define MIN16(a,b) ((a) < (b) ? (a) : (b)) /**< Maximum 16-bit value. */
|
||||
#define MAX16(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum 16-bit value. */
|
||||
#define ABS32(x) ((x) < 0 ? (-(x)) : (x)) /**< Absolute 32-bit value. */
|
||||
#define MIN32(a,b) ((a) < (b) ? (a) : (b)) /**< Maximum 32-bit value. */
|
||||
#define MAX32(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum 32-bit value. */
|
||||
|
||||
#ifdef FIXED_POINT
|
||||
|
||||
typedef spx_int16_t spx_word16_t;
|
||||
typedef spx_int32_t spx_word32_t;
|
||||
typedef spx_word32_t spx_mem_t;
|
||||
typedef spx_word16_t spx_coef_t;
|
||||
typedef spx_word16_t spx_lsp_t;
|
||||
typedef spx_word32_t spx_sig_t;
|
||||
|
||||
#define Q15ONE 32767
|
||||
|
||||
#define LPC_SCALING 8192
|
||||
#define SIG_SCALING 16384
|
||||
#define LSP_SCALING 8192.
|
||||
#define GAMMA_SCALING 32768.
|
||||
#define GAIN_SCALING 64
|
||||
#define GAIN_SCALING_1 0.015625
|
||||
|
||||
#define LPC_SHIFT 13
|
||||
#define LSP_SHIFT 13
|
||||
#define SIG_SHIFT 14
|
||||
#define GAIN_SHIFT 6
|
||||
|
||||
#define WORD2INT(x) ((x) < -32767 ? -32768 : ((x) > 32766 ? 32767 : (x)))
|
||||
|
||||
#define VERY_SMALL 0
|
||||
#define VERY_LARGE32 ((spx_word32_t)2147483647)
|
||||
#define VERY_LARGE16 ((spx_word16_t)32767)
|
||||
#define Q15_ONE ((spx_word16_t)32767)
|
||||
|
||||
|
||||
#ifdef FIXED_DEBUG
|
||||
#include "fixed_debug.h"
|
||||
#else
|
||||
|
||||
#include "fixed_generic.h"
|
||||
|
||||
#ifdef ARM5E_ASM
|
||||
#include "fixed_arm5e.h"
|
||||
#elif defined(ARM4_ASM)
|
||||
#include "fixed_arm4.h"
|
||||
#elif defined(BFIN_ASM)
|
||||
#include "fixed_bfin.h"
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
#else
|
||||
|
||||
typedef float spx_mem_t;
|
||||
typedef float spx_coef_t;
|
||||
typedef float spx_lsp_t;
|
||||
typedef float spx_sig_t;
|
||||
typedef float spx_word16_t;
|
||||
typedef float spx_word32_t;
|
||||
|
||||
#define Q15ONE 1.0f
|
||||
#define LPC_SCALING 1.f
|
||||
#define SIG_SCALING 1.f
|
||||
#define LSP_SCALING 1.f
|
||||
#define GAMMA_SCALING 1.f
|
||||
#define GAIN_SCALING 1.f
|
||||
#define GAIN_SCALING_1 1.f
|
||||
|
||||
|
||||
#define VERY_SMALL 1e-15f
|
||||
#define VERY_LARGE32 1e15f
|
||||
#define VERY_LARGE16 1e15f
|
||||
#define Q15_ONE ((spx_word16_t)1.f)
|
||||
|
||||
#define QCONST16(x,bits) (x)
|
||||
#define QCONST32(x,bits) (x)
|
||||
|
||||
#define NEG16(x) (-(x))
|
||||
#define NEG32(x) (-(x))
|
||||
#define EXTRACT16(x) (x)
|
||||
#define EXTEND32(x) (x)
|
||||
#define SHR16(a,shift) (a)
|
||||
#define SHL16(a,shift) (a)
|
||||
#define SHR32(a,shift) (a)
|
||||
#define SHL32(a,shift) (a)
|
||||
#define PSHR16(a,shift) (a)
|
||||
#define PSHR32(a,shift) (a)
|
||||
#define VSHR32(a,shift) (a)
|
||||
#define SATURATE16(x,a) (x)
|
||||
#define SATURATE32(x,a) (x)
|
||||
#define SATURATE32PSHR(x,shift,a) (x)
|
||||
|
||||
#define PSHR(a,shift) (a)
|
||||
#define SHR(a,shift) (a)
|
||||
#define SHL(a,shift) (a)
|
||||
#define SATURATE(x,a) (x)
|
||||
|
||||
#define ADD16(a,b) ((a)+(b))
|
||||
#define SUB16(a,b) ((a)-(b))
|
||||
#define ADD32(a,b) ((a)+(b))
|
||||
#define SUB32(a,b) ((a)-(b))
|
||||
#define MULT16_16_16(a,b) ((a)*(b))
|
||||
#define MULT16_32_32(a,b) ((a)*(b))
|
||||
#define MULT16_16(a,b) ((spx_word32_t)(a)*(spx_word32_t)(b))
|
||||
#define MAC16_16(c,a,b) ((c)+(spx_word32_t)(a)*(spx_word32_t)(b))
|
||||
|
||||
#define MULT16_32_Q15(a,b) ((a)*(b))
|
||||
#define MULT16_32_P15(a,b) ((a)*(b))
|
||||
|
||||
#define MAC16_32_Q15(c,a,b) ((c)+(a)*(b))
|
||||
|
||||
#define MAC16_16_Q11(c,a,b) ((c)+(a)*(b))
|
||||
#define MAC16_16_Q13(c,a,b) ((c)+(a)*(b))
|
||||
#define MAC16_16_P13(c,a,b) ((c)+(a)*(b))
|
||||
#define MULT16_16_Q11_32(a,b) ((a)*(b))
|
||||
#define MULT16_16_Q13(a,b) ((a)*(b))
|
||||
#define MULT16_16_Q14(a,b) ((a)*(b))
|
||||
#define MULT16_16_Q15(a,b) ((a)*(b))
|
||||
#define MULT16_16_P15(a,b) ((a)*(b))
|
||||
#define MULT16_16_P13(a,b) ((a)*(b))
|
||||
#define MULT16_16_P14(a,b) ((a)*(b))
|
||||
|
||||
#define DIV32_16(a,b) (((spx_word32_t)(a))/(spx_word16_t)(b))
|
||||
#define PDIV32_16(a,b) (((spx_word32_t)(a))/(spx_word16_t)(b))
|
||||
#define DIV32(a,b) (((spx_word32_t)(a))/(spx_word32_t)(b))
|
||||
#define PDIV32(a,b) (((spx_word32_t)(a))/(spx_word32_t)(b))
|
||||
|
||||
#define WORD2INT(x) ((x) < -32767.5f ? -32768 : \
|
||||
((x) > 32766.5f ? 32767 : (spx_int16_t)floor(.5 + (x))))
|
||||
#endif
|
||||
|
||||
|
||||
#if defined(CONFIG_TI_C54X) || defined(CONFIG_TI_C55X)
|
||||
|
||||
/* 2 on TI C5x DSP */
|
||||
#define BYTES_PER_CHAR 2
|
||||
#define BITS_PER_CHAR 16
|
||||
#define LOG2_BITS_PER_CHAR 4
|
||||
|
||||
#else
|
||||
|
||||
#define BYTES_PER_CHAR 1
|
||||
#define BITS_PER_CHAR 8
|
||||
#define LOG2_BITS_PER_CHAR 3
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
#ifdef FIXED_DEBUG
|
||||
extern long long spx_mips;
|
||||
#endif
|
||||
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,448 @@
|
||||
/* Copyright (C) 2005-2006 Jean-Marc Valin
|
||||
File: fftwrap.c
|
||||
|
||||
Wrapper for various FFTs
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
- Neither the name of the Xiph.org Foundation nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "arch.h"
|
||||
#include "os_support.h"
|
||||
|
||||
#define MAX_FFT_SIZE 2048
|
||||
|
||||
#ifdef FIXED_POINT
|
||||
static int maximize_range(spx_word16_t *in, spx_word16_t *out, spx_word16_t bound, int len)
|
||||
{
|
||||
int i, shift;
|
||||
spx_word16_t max_val = 0;
|
||||
for (i=0;i<len;i++)
|
||||
{
|
||||
if (in[i]>max_val)
|
||||
max_val = in[i];
|
||||
if (-in[i]>max_val)
|
||||
max_val = -in[i];
|
||||
}
|
||||
shift=0;
|
||||
while (max_val <= (bound>>1) && max_val != 0)
|
||||
{
|
||||
max_val <<= 1;
|
||||
shift++;
|
||||
}
|
||||
for (i=0;i<len;i++)
|
||||
{
|
||||
out[i] = SHL16(in[i], shift);
|
||||
}
|
||||
return shift;
|
||||
}
|
||||
|
||||
static void renorm_range(spx_word16_t *in, spx_word16_t *out, int shift, int len)
|
||||
{
|
||||
int i;
|
||||
for (i=0;i<len;i++)
|
||||
{
|
||||
out[i] = PSHR16(in[i], shift);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SMALLFT
|
||||
|
||||
#include "smallft.h"
|
||||
#include <math.h>
|
||||
|
||||
void *spx_fft_init(int size)
|
||||
{
|
||||
struct drft_lookup *table;
|
||||
table = speex_alloc(sizeof(struct drft_lookup));
|
||||
spx_drft_init((struct drft_lookup *)table, size);
|
||||
return (void*)table;
|
||||
}
|
||||
|
||||
void spx_fft_destroy(void *table)
|
||||
{
|
||||
spx_drft_clear(table);
|
||||
speex_free(table);
|
||||
}
|
||||
|
||||
void spx_fft(void *table, float *in, float *out)
|
||||
{
|
||||
if (in==out)
|
||||
{
|
||||
int i;
|
||||
float scale = 1./((struct drft_lookup *)table)->n;
|
||||
speex_warning("FFT should not be done in-place");
|
||||
for (i=0;i<((struct drft_lookup *)table)->n;i++)
|
||||
out[i] = scale*in[i];
|
||||
} else {
|
||||
int i;
|
||||
float scale = 1./((struct drft_lookup *)table)->n;
|
||||
for (i=0;i<((struct drft_lookup *)table)->n;i++)
|
||||
out[i] = scale*in[i];
|
||||
}
|
||||
spx_drft_forward((struct drft_lookup *)table, out);
|
||||
}
|
||||
|
||||
void spx_ifft(void *table, float *in, float *out)
|
||||
{
|
||||
if (in==out)
|
||||
{
|
||||
speex_warning("FFT should not be done in-place");
|
||||
} else {
|
||||
int i;
|
||||
for (i=0;i<((struct drft_lookup *)table)->n;i++)
|
||||
out[i] = in[i];
|
||||
}
|
||||
spx_drft_backward((struct drft_lookup *)table, out);
|
||||
}
|
||||
|
||||
#elif defined(USE_INTEL_MKL)
|
||||
#include <mkl.h>
|
||||
|
||||
struct mkl_config {
|
||||
DFTI_DESCRIPTOR_HANDLE desc;
|
||||
int N;
|
||||
};
|
||||
|
||||
void *spx_fft_init(int size)
|
||||
{
|
||||
struct mkl_config *table = (struct mkl_config *) speex_alloc(sizeof(struct mkl_config));
|
||||
table->N = size;
|
||||
DftiCreateDescriptor(&table->desc, DFTI_SINGLE, DFTI_REAL, 1, size);
|
||||
DftiSetValue(table->desc, DFTI_PACKED_FORMAT, DFTI_PACK_FORMAT);
|
||||
DftiSetValue(table->desc, DFTI_PLACEMENT, DFTI_NOT_INPLACE);
|
||||
DftiSetValue(table->desc, DFTI_FORWARD_SCALE, 1.0f / size);
|
||||
DftiCommitDescriptor(table->desc);
|
||||
return table;
|
||||
}
|
||||
|
||||
void spx_fft_destroy(void *table)
|
||||
{
|
||||
struct mkl_config *t = (struct mkl_config *) table;
|
||||
DftiFreeDescriptor(t->desc);
|
||||
speex_free(table);
|
||||
}
|
||||
|
||||
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out)
|
||||
{
|
||||
struct mkl_config *t = (struct mkl_config *) table;
|
||||
DftiComputeForward(t->desc, in, out);
|
||||
}
|
||||
|
||||
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out)
|
||||
{
|
||||
struct mkl_config *t = (struct mkl_config *) table;
|
||||
DftiComputeBackward(t->desc, in, out);
|
||||
}
|
||||
|
||||
#elif defined(USE_INTEL_IPP)
|
||||
|
||||
#include <ipps.h>
|
||||
|
||||
struct ipp_fft_config
|
||||
{
|
||||
IppsDFTSpec_R_32f *dftSpec;
|
||||
Ipp8u *buffer;
|
||||
};
|
||||
|
||||
void *spx_fft_init(int size)
|
||||
{
|
||||
int bufferSize = 0;
|
||||
int hint;
|
||||
struct ipp_fft_config *table;
|
||||
|
||||
table = (struct ipp_fft_config *)speex_alloc(sizeof(struct ipp_fft_config));
|
||||
|
||||
/* there appears to be no performance difference between ippAlgHintFast and
|
||||
ippAlgHintAccurate when using the with the floating point version
|
||||
of the fft. */
|
||||
hint = ippAlgHintAccurate;
|
||||
|
||||
ippsDFTInitAlloc_R_32f(&table->dftSpec, size, IPP_FFT_DIV_FWD_BY_N, hint);
|
||||
|
||||
ippsDFTGetBufSize_R_32f(table->dftSpec, &bufferSize);
|
||||
table->buffer = ippsMalloc_8u(bufferSize);
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
void spx_fft_destroy(void *table)
|
||||
{
|
||||
struct ipp_fft_config *t = (struct ipp_fft_config *)table;
|
||||
ippsFree(t->buffer);
|
||||
ippsDFTFree_R_32f(t->dftSpec);
|
||||
speex_free(t);
|
||||
}
|
||||
|
||||
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out)
|
||||
{
|
||||
struct ipp_fft_config *t = (struct ipp_fft_config *)table;
|
||||
ippsDFTFwd_RToPack_32f(in, out, t->dftSpec, t->buffer);
|
||||
}
|
||||
|
||||
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out)
|
||||
{
|
||||
struct ipp_fft_config *t = (struct ipp_fft_config *)table;
|
||||
ippsDFTInv_PackToR_32f(in, out, t->dftSpec, t->buffer);
|
||||
}
|
||||
|
||||
#elif defined(USE_GPL_FFTW3)
|
||||
|
||||
#include <fftw3.h>
|
||||
|
||||
struct fftw_config {
|
||||
float *in;
|
||||
float *out;
|
||||
fftwf_plan fft;
|
||||
fftwf_plan ifft;
|
||||
int N;
|
||||
};
|
||||
|
||||
void *spx_fft_init(int size)
|
||||
{
|
||||
struct fftw_config *table = (struct fftw_config *) speex_alloc(sizeof(struct fftw_config));
|
||||
table->in = fftwf_malloc(sizeof(float) * (size+2));
|
||||
table->out = fftwf_malloc(sizeof(float) * (size+2));
|
||||
|
||||
table->fft = fftwf_plan_dft_r2c_1d(size, table->in, (fftwf_complex *) table->out, FFTW_PATIENT);
|
||||
table->ifft = fftwf_plan_dft_c2r_1d(size, (fftwf_complex *) table->in, table->out, FFTW_PATIENT);
|
||||
|
||||
table->N = size;
|
||||
return table;
|
||||
}
|
||||
|
||||
void spx_fft_destroy(void *table)
|
||||
{
|
||||
struct fftw_config *t = (struct fftw_config *) table;
|
||||
fftwf_destroy_plan(t->fft);
|
||||
fftwf_destroy_plan(t->ifft);
|
||||
fftwf_free(t->in);
|
||||
fftwf_free(t->out);
|
||||
speex_free(table);
|
||||
}
|
||||
|
||||
|
||||
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out)
|
||||
{
|
||||
int i;
|
||||
struct fftw_config *t = (struct fftw_config *) table;
|
||||
const int N = t->N;
|
||||
float *iptr = t->in;
|
||||
float *optr = t->out;
|
||||
const float m = 1.0 / N;
|
||||
for(i=0;i<N;++i)
|
||||
iptr[i]=in[i] * m;
|
||||
|
||||
fftwf_execute(t->fft);
|
||||
|
||||
out[0] = optr[0];
|
||||
for(i=1;i<N;++i)
|
||||
out[i] = optr[i+1];
|
||||
}
|
||||
|
||||
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out)
|
||||
{
|
||||
int i;
|
||||
struct fftw_config *t = (struct fftw_config *) table;
|
||||
const int N = t->N;
|
||||
float *iptr = t->in;
|
||||
float *optr = t->out;
|
||||
|
||||
iptr[0] = in[0];
|
||||
iptr[1] = 0.0f;
|
||||
for(i=1;i<N;++i)
|
||||
iptr[i+1] = in[i];
|
||||
iptr[N+1] = 0.0f;
|
||||
|
||||
fftwf_execute(t->ifft);
|
||||
|
||||
for(i=0;i<N;++i)
|
||||
out[i] = optr[i];
|
||||
}
|
||||
|
||||
#elif defined(USE_KISS_FFT)
|
||||
|
||||
#include "kiss_fftr.h"
|
||||
#include "kiss_fft.h"
|
||||
|
||||
struct kiss_config {
|
||||
kiss_fftr_cfg forward;
|
||||
kiss_fftr_cfg backward;
|
||||
int N;
|
||||
};
|
||||
|
||||
void *spx_fft_init(int size)
|
||||
{
|
||||
struct kiss_config *table;
|
||||
table = (struct kiss_config*)speex_alloc(sizeof(struct kiss_config));
|
||||
table->forward = kiss_fftr_alloc(size,0,NULL,NULL);
|
||||
table->backward = kiss_fftr_alloc(size,1,NULL,NULL);
|
||||
table->N = size;
|
||||
return table;
|
||||
}
|
||||
|
||||
void spx_fft_destroy(void *table)
|
||||
{
|
||||
struct kiss_config *t = (struct kiss_config *)table;
|
||||
kiss_fftr_free(t->forward);
|
||||
kiss_fftr_free(t->backward);
|
||||
speex_free(table);
|
||||
}
|
||||
|
||||
#ifdef FIXED_POINT
|
||||
|
||||
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out)
|
||||
{
|
||||
int shift;
|
||||
struct kiss_config *t = (struct kiss_config *)table;
|
||||
shift = maximize_range(in, in, 32000, t->N);
|
||||
kiss_fftr2(t->forward, in, out);
|
||||
renorm_range(in, in, shift, t->N);
|
||||
renorm_range(out, out, shift, t->N);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out)
|
||||
{
|
||||
int i;
|
||||
float scale;
|
||||
struct kiss_config *t = (struct kiss_config *)table;
|
||||
scale = 1./t->N;
|
||||
kiss_fftr2(t->forward, in, out);
|
||||
for (i=0;i<t->N;i++)
|
||||
out[i] *= scale;
|
||||
}
|
||||
#endif
|
||||
|
||||
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out)
|
||||
{
|
||||
struct kiss_config *t = (struct kiss_config *)table;
|
||||
kiss_fftri2(t->backward, in, out);
|
||||
}
|
||||
|
||||
|
||||
#else
|
||||
|
||||
#error No other FFT implemented
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef FIXED_POINT
|
||||
/*#include "smallft.h"*/
|
||||
|
||||
|
||||
void spx_fft_float(void *table, float *in, float *out)
|
||||
{
|
||||
int i;
|
||||
#ifdef USE_SMALLFT
|
||||
int N = ((struct drft_lookup *)table)->n;
|
||||
#elif defined(USE_KISS_FFT)
|
||||
int N = ((struct kiss_config *)table)->N;
|
||||
#else
|
||||
#endif
|
||||
#ifdef VAR_ARRAYS
|
||||
spx_word16_t _in[N];
|
||||
spx_word16_t _out[N];
|
||||
#else
|
||||
spx_word16_t _in[MAX_FFT_SIZE];
|
||||
spx_word16_t _out[MAX_FFT_SIZE];
|
||||
#endif
|
||||
for (i=0;i<N;i++)
|
||||
_in[i] = (int)floor(.5+in[i]);
|
||||
spx_fft(table, _in, _out);
|
||||
for (i=0;i<N;i++)
|
||||
out[i] = _out[i];
|
||||
#if 0
|
||||
if (!fixed_point)
|
||||
{
|
||||
float scale;
|
||||
struct drft_lookup t;
|
||||
spx_drft_init(&t, ((struct kiss_config *)table)->N);
|
||||
scale = 1./((struct kiss_config *)table)->N;
|
||||
for (i=0;i<((struct kiss_config *)table)->N;i++)
|
||||
out[i] = scale*in[i];
|
||||
spx_drft_forward(&t, out);
|
||||
spx_drft_clear(&t);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void spx_ifft_float(void *table, float *in, float *out)
|
||||
{
|
||||
int i;
|
||||
#ifdef USE_SMALLFT
|
||||
int N = ((struct drft_lookup *)table)->n;
|
||||
#elif defined(USE_KISS_FFT)
|
||||
int N = ((struct kiss_config *)table)->N;
|
||||
#else
|
||||
#endif
|
||||
#ifdef VAR_ARRAYS
|
||||
spx_word16_t _in[N];
|
||||
spx_word16_t _out[N];
|
||||
#else
|
||||
spx_word16_t _in[MAX_FFT_SIZE];
|
||||
spx_word16_t _out[MAX_FFT_SIZE];
|
||||
#endif
|
||||
for (i=0;i<N;i++)
|
||||
_in[i] = (int)floor(.5+in[i]);
|
||||
spx_ifft(table, _in, _out);
|
||||
for (i=0;i<N;i++)
|
||||
out[i] = _out[i];
|
||||
#if 0
|
||||
if (!fixed_point)
|
||||
{
|
||||
int i;
|
||||
struct drft_lookup t;
|
||||
spx_drft_init(&t, ((struct kiss_config *)table)->N);
|
||||
for (i=0;i<((struct kiss_config *)table)->N;i++)
|
||||
out[i] = in[i];
|
||||
spx_drft_backward(&t, out);
|
||||
spx_drft_clear(&t);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void spx_fft_float(void *table, float *in, float *out)
|
||||
{
|
||||
spx_fft(table, in, out);
|
||||
}
|
||||
void spx_ifft_float(void *table, float *in, float *out)
|
||||
{
|
||||
spx_ifft(table, in, out);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,58 @@
|
||||
/* Copyright (C) 2005 Jean-Marc Valin
|
||||
File: fftwrap.h
|
||||
|
||||
Wrapper for various FFTs
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
- Neither the name of the Xiph.org Foundation nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
*/
|
||||
|
||||
#ifndef FFTWRAP_H
|
||||
#define FFTWRAP_H
|
||||
|
||||
#include "arch.h"
|
||||
|
||||
/** Compute tables for an FFT */
|
||||
void *spx_fft_init(int size);
|
||||
|
||||
/** Destroy tables for an FFT */
|
||||
void spx_fft_destroy(void *table);
|
||||
|
||||
/** Forward (real to half-complex) transform */
|
||||
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out);
|
||||
|
||||
/** Backward (half-complex to real) transform */
|
||||
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out);
|
||||
|
||||
/** Forward (real to half-complex) transform of float data */
|
||||
void spx_fft_float(void *table, float *in, float *out);
|
||||
|
||||
/** Backward (half-complex to real) transform of float data */
|
||||
void spx_ifft_float(void *table, float *in, float *out);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,170 @@
|
||||
/* Copyright (C) Jean-Marc Valin */
|
||||
/**
|
||||
@file speex_echo.h
|
||||
@brief Echo cancellation
|
||||
*/
|
||||
/*
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. The name of the author may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
|
||||
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#ifndef SPEEX_ECHO_H
|
||||
#define SPEEX_ECHO_H
|
||||
/** @defgroup SpeexEchoState SpeexEchoState: Acoustic echo canceller
|
||||
* This is the acoustic echo canceller module.
|
||||
* @{
|
||||
*/
|
||||
#include "speexdsp_types.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Obtain frame size used by the AEC */
|
||||
#define SPEEX_ECHO_GET_FRAME_SIZE 3
|
||||
|
||||
/** Set sampling rate */
|
||||
#define SPEEX_ECHO_SET_SAMPLING_RATE 24
|
||||
/** Get sampling rate */
|
||||
#define SPEEX_ECHO_GET_SAMPLING_RATE 25
|
||||
|
||||
/* Can't set window sizes */
|
||||
/** Get size of impulse response (int32) */
|
||||
#define SPEEX_ECHO_GET_IMPULSE_RESPONSE_SIZE 27
|
||||
|
||||
/* Can't set window content */
|
||||
/** Get impulse response (int32[]) */
|
||||
#define SPEEX_ECHO_GET_IMPULSE_RESPONSE 29
|
||||
|
||||
/** Internal echo canceller state. Should never be accessed directly. */
|
||||
struct SpeexEchoState_;
|
||||
|
||||
/** @class SpeexEchoState
|
||||
* This holds the state of the echo canceller. You need one per channel.
|
||||
*/
|
||||
|
||||
/** Internal echo canceller state. Should never be accessed directly. */
|
||||
typedef struct SpeexEchoState_ SpeexEchoState;
|
||||
|
||||
/** Creates a new echo canceller state
|
||||
* @param frame_size Number of samples to process at one time (should correspond to 10-20 ms)
|
||||
* @param filter_length Number of samples of echo to cancel (should generally correspond to 100-500 ms)
|
||||
* @return Newly-created echo canceller state
|
||||
*/
|
||||
SpeexEchoState *speex_echo_state_init(int frame_size, int filter_length);
|
||||
|
||||
/** Creates a new multi-channel echo canceller state
|
||||
* @param frame_size Number of samples to process at one time (should correspond to 10-20 ms)
|
||||
* @param filter_length Number of samples of echo to cancel (should generally correspond to 100-500 ms)
|
||||
* @param nb_mic Number of microphone channels
|
||||
* @param nb_speakers Number of speaker channels
|
||||
* @return Newly-created echo canceller state
|
||||
*/
|
||||
SpeexEchoState *speex_echo_state_init_mc(int frame_size, int filter_length, int nb_mic, int nb_speakers);
|
||||
|
||||
/** Destroys an echo canceller state
|
||||
* @param st Echo canceller state
|
||||
*/
|
||||
void speex_echo_state_destroy(SpeexEchoState *st);
|
||||
|
||||
/** Performs echo cancellation a frame, based on the audio sent to the speaker (no delay is added
|
||||
* to playback in this form)
|
||||
*
|
||||
* @param st Echo canceller state
|
||||
* @param rec Signal from the microphone (near end + far end echo)
|
||||
* @param play Signal played to the speaker (received from far end)
|
||||
* @param out Returns near-end signal with echo removed
|
||||
*/
|
||||
void speex_echo_cancellation(SpeexEchoState *st, const spx_int16_t *rec, const spx_int16_t *play, spx_int16_t *out);
|
||||
|
||||
/** Performs echo cancellation a frame (deprecated) */
|
||||
void speex_echo_cancel(SpeexEchoState *st, const spx_int16_t *rec, const spx_int16_t *play, spx_int16_t *out, spx_int32_t *Yout);
|
||||
|
||||
/** Perform echo cancellation using internal playback buffer, which is delayed by two frames
|
||||
* to account for the delay introduced by most soundcards (but it could be off!)
|
||||
* @param st Echo canceller state
|
||||
* @param rec Signal from the microphone (near end + far end echo)
|
||||
* @param out Returns near-end signal with echo removed
|
||||
*/
|
||||
void speex_echo_capture(SpeexEchoState *st, const spx_int16_t *rec, spx_int16_t *out);
|
||||
|
||||
/** Let the echo canceller know that a frame was just queued to the soundcard
|
||||
* @param st Echo canceller state
|
||||
* @param play Signal played to the speaker (received from far end)
|
||||
*/
|
||||
void speex_echo_playback(SpeexEchoState *st, const spx_int16_t *play);
|
||||
|
||||
/** Reset the echo canceller to its original state
|
||||
* @param st Echo canceller state
|
||||
*/
|
||||
void speex_echo_state_reset(SpeexEchoState *st);
|
||||
|
||||
/** Used like the ioctl function to control the echo canceller parameters
|
||||
*
|
||||
* @param st Echo canceller state
|
||||
* @param request ioctl-type request (one of the SPEEX_ECHO_* macros)
|
||||
* @param ptr Data exchanged to-from function
|
||||
* @return 0 if no error, -1 if request in unknown
|
||||
*/
|
||||
int speex_echo_ctl(SpeexEchoState *st, int request, void *ptr);
|
||||
|
||||
|
||||
|
||||
struct SpeexDecorrState_;
|
||||
|
||||
typedef struct SpeexDecorrState_ SpeexDecorrState;
|
||||
|
||||
|
||||
/** Create a state for the channel decorrelation algorithm
|
||||
This is useful for multi-channel echo cancellation only
|
||||
* @param rate Sampling rate
|
||||
* @param channels Number of channels (it's a bit pointless if you don't have at least 2)
|
||||
* @param frame_size Size of the frame to process at ones (counting samples *per* channel)
|
||||
*/
|
||||
SpeexDecorrState *speex_decorrelate_new(int rate, int channels, int frame_size);
|
||||
|
||||
/** Remove correlation between the channels by modifying the phase and possibly
|
||||
adding noise in a way that is not (or little) perceptible.
|
||||
* @param st Decorrelator state
|
||||
* @param in Input audio in interleaved format
|
||||
* @param out Result of the decorrelation (out *may* alias in)
|
||||
* @param strength How much alteration of the audio to apply from 0 to 100.
|
||||
*/
|
||||
void speex_decorrelate(SpeexDecorrState *st, const spx_int16_t *in, spx_int16_t *out, int strength);
|
||||
|
||||
/** Destroy a Decorrelation state
|
||||
* @param st State to destroy
|
||||
*/
|
||||
void speex_decorrelate_destroy(SpeexDecorrState *st);
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
/** @}*/
|
||||
#endif
|
||||
Loaded 100 of 1558 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user