lvthn-codec 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codec.py +149 -0
- core/__init__.py +0 -0
- core/audio.py +330 -0
- core/container.py +25 -0
- core/image.py +267 -0
- lvthn_codec-1.0.0.dist-info/METADATA +136 -0
- lvthn_codec-1.0.0.dist-info/RECORD +11 -0
- lvthn_codec-1.0.0.dist-info/WHEEL +5 -0
- lvthn_codec-1.0.0.dist-info/licenses/LICENSE +21 -0
- lvthn_codec-1.0.0.dist-info/licenses/PATENTS +30 -0
- lvthn_codec-1.0.0.dist-info/top_level.txt +2 -0
codec.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import numpy as np
|
|
5
|
+
from scipy.io import wavfile
|
|
6
|
+
|
|
7
|
+
from core import container, audio, image
|
|
8
|
+
|
|
9
|
+
PROFILES = {
|
|
10
|
+
'default': {'delta': 0.18, 'snr': 24.0},
|
|
11
|
+
'compact': {'delta': 0.25, 'snr': 20.0},
|
|
12
|
+
'detail': {'delta': 0.05, 'snr': 24.0},
|
|
13
|
+
'speech': {'delta': 0.10, 'snr': 20.0},
|
|
14
|
+
'master': {'delta': 0.03, 'snr': 30.0},
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
IMG_PROFILES = {
|
|
18
|
+
'fast': {'quality': 85, 'level': 1},
|
|
19
|
+
'default': {'quality': 92, 'level': 12},
|
|
20
|
+
'max': {'quality': 98, 'level': 19},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def encode_pcm(pcm, sr, ch, delta, target_snr_db):
|
|
24
|
+
payload = audio.encode_audio(pcm, sr, ch, delta, target_snr_db)
|
|
25
|
+
return container.wrap(container.MEDIA_AUDIO, payload)
|
|
26
|
+
|
|
27
|
+
def decode_lvn(data):
|
|
28
|
+
media_type, payload = container.unwrap(data)
|
|
29
|
+
if media_type == container.MEDIA_AUDIO:
|
|
30
|
+
return audio.decode_audio(payload)
|
|
31
|
+
elif media_type == container.MEDIA_IMAGE:
|
|
32
|
+
return image.decode_image(payload)
|
|
33
|
+
else:
|
|
34
|
+
raise ValueError(f'Unsupported media_type: 0x{media_type:02x}')
|
|
35
|
+
|
|
36
|
+
def decode_lvn_info(data):
|
|
37
|
+
media_type, payload = container.unwrap(data)
|
|
38
|
+
if media_type == container.MEDIA_AUDIO:
|
|
39
|
+
info = audio.get_audio_info(payload)
|
|
40
|
+
info['media_type'] = media_type
|
|
41
|
+
info['total'] = len(data)
|
|
42
|
+
return info
|
|
43
|
+
elif media_type == container.MEDIA_IMAGE:
|
|
44
|
+
info = image.get_image_info(payload)
|
|
45
|
+
info['media_type'] = media_type
|
|
46
|
+
info['total'] = len(data)
|
|
47
|
+
return info
|
|
48
|
+
return dict(media_type=media_type, total=len(data))
|
|
49
|
+
|
|
50
|
+
def read_wav(path):
|
|
51
|
+
sr, d = wavfile.read(path)
|
|
52
|
+
if d.dtype == np.int16: d = d.astype(np.float64) / 32768.0
|
|
53
|
+
elif d.dtype == np.int32: d = d.astype(np.float64) / 2147483648.0
|
|
54
|
+
elif d.dtype == np.uint8: d = (d.astype(np.float64) - 128.0) / 128.0
|
|
55
|
+
else: d = d.astype(np.float64)
|
|
56
|
+
if d.ndim == 1: d = d[:, None]
|
|
57
|
+
return d, sr
|
|
58
|
+
|
|
59
|
+
def write_wav(path, pcm, sr):
|
|
60
|
+
wavfile.write(path, sr, np.clip(pcm * 32767, -32768, 32767).astype(np.int16))
|
|
61
|
+
|
|
62
|
+
def read_image(path):
|
|
63
|
+
from PIL import Image
|
|
64
|
+
img = Image.open(path)
|
|
65
|
+
if img.mode == 'RGBA':
|
|
66
|
+
return np.array(img), 4
|
|
67
|
+
else:
|
|
68
|
+
return np.array(img.convert('RGB')), 3
|
|
69
|
+
|
|
70
|
+
def write_image(path, pixels, channels):
|
|
71
|
+
from PIL import Image
|
|
72
|
+
img = Image.fromarray(pixels)
|
|
73
|
+
img.save(path)
|
|
74
|
+
|
|
75
|
+
def main():
|
|
76
|
+
if len(sys.argv) < 2:
|
|
77
|
+
print('LVTHN-CODEC')
|
|
78
|
+
print('Commands: encode | decode | info | encode-img | decode-img | profiles')
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
cmd, args = sys.argv[1], sys.argv[2:]
|
|
81
|
+
|
|
82
|
+
if cmd == 'encode':
|
|
83
|
+
if len(args) < 2: sys.exit('usage: encode in.wav out.lvn [profile|delta] [target_snr_db]')
|
|
84
|
+
pcm, sr = read_wav(args[0])
|
|
85
|
+
arg2 = args[2] if len(args) > 2 else 'default'
|
|
86
|
+
if arg2 in PROFILES:
|
|
87
|
+
delta = PROFILES[arg2]['delta']
|
|
88
|
+
tsnr = PROFILES[arg2]['snr']
|
|
89
|
+
print(f'[audio profile: {arg2}] delta={delta} snr={tsnr}')
|
|
90
|
+
else:
|
|
91
|
+
delta = float(arg2)
|
|
92
|
+
tsnr = float(args[3]) if len(args) > 3 else 24.0
|
|
93
|
+
|
|
94
|
+
t0 = time.time()
|
|
95
|
+
data = encode_pcm(pcm, sr, pcm.shape[1], delta, target_snr_db=tsnr)
|
|
96
|
+
Path(args[1]).write_bytes(data)
|
|
97
|
+
print(f'OK {args[1]} {len(data)} bytes ({pcm.size * 2 / len(data):.2f}x vs PCM) encode={time.time() - t0:.2f}s')
|
|
98
|
+
|
|
99
|
+
elif cmd == 'decode':
|
|
100
|
+
if len(args) < 2: sys.exit('usage: decode in.lvn out.wav')
|
|
101
|
+
pcm, sr, ch, n = decode_lvn(Path(args[0]).read_bytes())
|
|
102
|
+
write_wav(args[1], pcm, sr)
|
|
103
|
+
print(f'OK {args[1]} {n} samples, {ch} ch, {sr} Hz')
|
|
104
|
+
|
|
105
|
+
elif cmd == 'encode-img':
|
|
106
|
+
if len(args) < 2: sys.exit('usage: encode-img in.png out.lvn [profile|quality] [level]')
|
|
107
|
+
pixels, ch = read_image(args[0])
|
|
108
|
+
arg2 = args[2] if len(args) > 2 else 'default'
|
|
109
|
+
if arg2 in IMG_PROFILES:
|
|
110
|
+
q = IMG_PROFILES[arg2]['quality']
|
|
111
|
+
lvl = IMG_PROFILES[arg2]['level']
|
|
112
|
+
print(f'[image profile: {arg2}] quality={q} level={lvl}')
|
|
113
|
+
else:
|
|
114
|
+
q = int(arg2)
|
|
115
|
+
lvl = int(args[3]) if len(args) > 3 else 19
|
|
116
|
+
|
|
117
|
+
t0 = time.time()
|
|
118
|
+
payload = image.encode_image(pixels, pixels.shape[1], pixels.shape[0], ch, q, lvl)
|
|
119
|
+
data = container.wrap(container.MEDIA_IMAGE, payload)
|
|
120
|
+
Path(args[1]).write_bytes(data)
|
|
121
|
+
print(f'OK {args[1]} {len(data)} bytes encode={time.time() - t0:.2f}s')
|
|
122
|
+
|
|
123
|
+
elif cmd == 'decode-img':
|
|
124
|
+
if len(args) < 2: sys.exit('usage: decode-img in.lvn out.png')
|
|
125
|
+
mt, payload = container.unwrap(Path(args[0]).read_bytes())
|
|
126
|
+
if mt != container.MEDIA_IMAGE: sys.exit('Not an image')
|
|
127
|
+
pixels = image.decode_image(payload)
|
|
128
|
+
write_image(args[1], pixels, pixels.shape[2] if len(pixels.shape) > 2 else 1)
|
|
129
|
+
print(f'OK {args[1]}')
|
|
130
|
+
|
|
131
|
+
elif cmd == 'info':
|
|
132
|
+
if not args: sys.exit('usage: info file.lvn')
|
|
133
|
+
info = decode_lvn_info(Path(args[0]).read_bytes())
|
|
134
|
+
if info['media_type'] == container.MEDIA_AUDIO:
|
|
135
|
+
print(f"LVTHN v1 audio {info['sr']} Hz {info['ch']} ch {info['n_samples'] / info['sr']:.2f}s delta={info['delta']:.4f}")
|
|
136
|
+
print(f" target_snr_db: {info['target_snr_db']:.1f} use_ms: {info['use_ms']} total: {info['total']}")
|
|
137
|
+
elif info['media_type'] == container.MEDIA_IMAGE:
|
|
138
|
+
print(f"LVTHN v1 image {info['width']}x{info['height']} alpha={info['has_alpha']} quality={info['quality']:.0f} total: {info['total']}")
|
|
139
|
+
|
|
140
|
+
elif cmd == 'profiles':
|
|
141
|
+
print('Audio profiles:')
|
|
142
|
+
for name, p in PROFILES.items():
|
|
143
|
+
print(f' {name:10s} delta={p["delta"]:<6} snr={p["snr"]}')
|
|
144
|
+
print('Image profiles:')
|
|
145
|
+
for name, p in IMG_PROFILES.items():
|
|
146
|
+
print(f' {name:10s} quality={p["quality"]:<4} level={p["level"]}')
|
|
147
|
+
|
|
148
|
+
if __name__ == '__main__':
|
|
149
|
+
main()
|
core/__init__.py
ADDED
|
File without changes
|
core/audio.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
import numpy as np
|
|
3
|
+
from numba import njit
|
|
4
|
+
|
|
5
|
+
FRAME_SIZE = 1024
|
|
6
|
+
HOP = FRAME_SIZE // 2
|
|
7
|
+
N_COEFFS = FRAME_SIZE // 2
|
|
8
|
+
|
|
9
|
+
DEFAULT_DELTA = 0.18
|
|
10
|
+
DEFAULT_TARGET_SNR_DB = 24.0
|
|
11
|
+
DEFAULT_DELTA_CURVE = np.array([1.0, 1.0, 1.1, 1.3, 1.6, 2.0, 2.5, 3.0], dtype=np.float32)
|
|
12
|
+
MIN_BAND_SIZE = 3
|
|
13
|
+
SCALE_MIN = -32
|
|
14
|
+
SCALE_MAX = 31
|
|
15
|
+
SCALE_BITS = 6
|
|
16
|
+
ATH_OFFSET_DB = 96.0
|
|
17
|
+
|
|
18
|
+
AUDIO_HDR_FMT = '<IBHQf8ffBI'
|
|
19
|
+
AUDIO_HDR_SIZE = struct.calcsize(AUDIO_HDR_FMT)
|
|
20
|
+
|
|
21
|
+
_WINDOW = np.sin(np.pi * (np.arange(FRAME_SIZE) + 0.5) / FRAME_SIZE).astype(np.float64)
|
|
22
|
+
_n = np.arange(FRAME_SIZE, dtype=np.float64)[:, None]
|
|
23
|
+
_k = np.arange(N_COEFFS, dtype=np.float64)[None, :]
|
|
24
|
+
_MDCT_MATRIX = np.cos(np.pi / N_COEFFS * (_n + 0.5 + N_COEFFS / 2) * (_k + 0.5))
|
|
25
|
+
_IMDCT_MATRIX = (_MDCT_MATRIX.T * (2.0 / N_COEFFS)).astype(np.float64)
|
|
26
|
+
|
|
27
|
+
@njit(cache=True, inline='always')
|
|
28
|
+
def _get_bit(buf, pos):
|
|
29
|
+
return (buf[pos >> 3] >> (7 - (pos & 7))) & 1
|
|
30
|
+
|
|
31
|
+
@njit(cache=True, inline='always')
|
|
32
|
+
def _set_bit(buf, pos, val):
|
|
33
|
+
if val:
|
|
34
|
+
buf[pos >> 3] |= (1 << (7 - (pos & 7)))
|
|
35
|
+
|
|
36
|
+
@njit(cache=True)
|
|
37
|
+
def _encode_frame_channel(quantized, band_starts, band_ends, scales, out_buf, bit_pos):
|
|
38
|
+
n_bands = len(band_starts)
|
|
39
|
+
n_coeffs = len(quantized)
|
|
40
|
+
last_nz = 0
|
|
41
|
+
for i in range(n_coeffs):
|
|
42
|
+
if quantized[i] != 0:
|
|
43
|
+
last_nz = i + 1
|
|
44
|
+
for i in range(9, -1, -1):
|
|
45
|
+
_set_bit(out_buf, bit_pos, (last_nz >> i) & 1)
|
|
46
|
+
bit_pos += 1
|
|
47
|
+
if last_nz == 0:
|
|
48
|
+
return bit_pos
|
|
49
|
+
|
|
50
|
+
band_present = np.zeros(n_bands, dtype=np.int32)
|
|
51
|
+
band_rice_k = np.zeros(n_bands, dtype=np.int32)
|
|
52
|
+
for b in range(n_bands):
|
|
53
|
+
s = band_starts[b]
|
|
54
|
+
e = band_ends[b]
|
|
55
|
+
if s >= last_nz:
|
|
56
|
+
break
|
|
57
|
+
be = min(e, last_nz)
|
|
58
|
+
any_nz = False
|
|
59
|
+
total_zz = 0
|
|
60
|
+
for i in range(s, be):
|
|
61
|
+
v = quantized[i]
|
|
62
|
+
if v != 0:
|
|
63
|
+
any_nz = True
|
|
64
|
+
total_zz += (v << 1) if v >= 0 else ((-v) << 1) - 1
|
|
65
|
+
if any_nz:
|
|
66
|
+
band_present[b] = 1
|
|
67
|
+
mean = total_zz / (be - s)
|
|
68
|
+
k = 0
|
|
69
|
+
m = mean
|
|
70
|
+
while m > 2.0 and k < 15:
|
|
71
|
+
m /= 2.0
|
|
72
|
+
k += 1
|
|
73
|
+
band_rice_k[b] = k
|
|
74
|
+
|
|
75
|
+
for b in range(n_bands):
|
|
76
|
+
s = band_starts[b]
|
|
77
|
+
if s >= last_nz:
|
|
78
|
+
break
|
|
79
|
+
_set_bit(out_buf, bit_pos, band_present[b])
|
|
80
|
+
bit_pos += 1
|
|
81
|
+
|
|
82
|
+
for b in range(n_bands):
|
|
83
|
+
s = band_starts[b]
|
|
84
|
+
e = band_ends[b]
|
|
85
|
+
if s >= last_nz:
|
|
86
|
+
break
|
|
87
|
+
if band_present[b] == 0:
|
|
88
|
+
continue
|
|
89
|
+
scale_enc = scales[b] - SCALE_MIN
|
|
90
|
+
for i in range(SCALE_BITS - 1, -1, -1):
|
|
91
|
+
_set_bit(out_buf, bit_pos, (scale_enc >> i) & 1)
|
|
92
|
+
bit_pos += 1
|
|
93
|
+
k = band_rice_k[b]
|
|
94
|
+
for i in range(3, -1, -1):
|
|
95
|
+
_set_bit(out_buf, bit_pos, (k >> i) & 1)
|
|
96
|
+
bit_pos += 1
|
|
97
|
+
be = min(e, last_nz)
|
|
98
|
+
for i in range(s, be):
|
|
99
|
+
v = quantized[i]
|
|
100
|
+
zz = (v << 1) if v >= 0 else ((-v) << 1) - 1
|
|
101
|
+
q = zz >> k
|
|
102
|
+
r = zz - (q << k)
|
|
103
|
+
for _ in range(q):
|
|
104
|
+
bit_pos += 1
|
|
105
|
+
_set_bit(out_buf, bit_pos, 1)
|
|
106
|
+
bit_pos += 1
|
|
107
|
+
for i2 in range(k - 1, -1, -1):
|
|
108
|
+
_set_bit(out_buf, bit_pos, (r >> i2) & 1)
|
|
109
|
+
bit_pos += 1
|
|
110
|
+
return bit_pos
|
|
111
|
+
|
|
112
|
+
@njit(cache=True)
|
|
113
|
+
def _decode_frame_channel(in_buf, bit_pos, quantized, scales, band_starts, band_ends):
|
|
114
|
+
n_bands = len(band_starts)
|
|
115
|
+
n_coeffs = len(quantized)
|
|
116
|
+
for i in range(n_coeffs):
|
|
117
|
+
quantized[i] = 0
|
|
118
|
+
for i in range(n_bands):
|
|
119
|
+
scales[i] = 0
|
|
120
|
+
|
|
121
|
+
last_nz = 0
|
|
122
|
+
for i in range(10):
|
|
123
|
+
last_nz = (last_nz << 1) | _get_bit(in_buf, bit_pos)
|
|
124
|
+
bit_pos += 1
|
|
125
|
+
if last_nz == 0:
|
|
126
|
+
return bit_pos
|
|
127
|
+
|
|
128
|
+
present = np.zeros(n_bands, dtype=np.int32)
|
|
129
|
+
for b in range(n_bands):
|
|
130
|
+
s = band_starts[b]
|
|
131
|
+
if s >= last_nz:
|
|
132
|
+
break
|
|
133
|
+
present[b] = _get_bit(in_buf, bit_pos)
|
|
134
|
+
bit_pos += 1
|
|
135
|
+
|
|
136
|
+
for b in range(n_bands):
|
|
137
|
+
s = band_starts[b]
|
|
138
|
+
e = band_ends[b]
|
|
139
|
+
if s >= last_nz:
|
|
140
|
+
break
|
|
141
|
+
if present[b] == 0:
|
|
142
|
+
continue
|
|
143
|
+
scale_enc = 0
|
|
144
|
+
for i in range(SCALE_BITS):
|
|
145
|
+
scale_enc = (scale_enc << 1) | _get_bit(in_buf, bit_pos)
|
|
146
|
+
bit_pos += 1
|
|
147
|
+
scales[b] = scale_enc + SCALE_MIN
|
|
148
|
+
k = 0
|
|
149
|
+
for i in range(4):
|
|
150
|
+
k = (k << 1) | _get_bit(in_buf, bit_pos)
|
|
151
|
+
bit_pos += 1
|
|
152
|
+
be = min(e, last_nz)
|
|
153
|
+
for i in range(s, be):
|
|
154
|
+
q = 0
|
|
155
|
+
while _get_bit(in_buf, bit_pos) == 0:
|
|
156
|
+
q += 1
|
|
157
|
+
bit_pos += 1
|
|
158
|
+
bit_pos += 1
|
|
159
|
+
r = 0
|
|
160
|
+
for i2 in range(k):
|
|
161
|
+
r = (r << 1) | _get_bit(in_buf, bit_pos)
|
|
162
|
+
bit_pos += 1
|
|
163
|
+
zz = (q << k) | r
|
|
164
|
+
quantized[i] = (zz >> 1) if (zz & 1) == 0 else -((zz + 1) >> 1)
|
|
165
|
+
return bit_pos
|
|
166
|
+
|
|
167
|
+
@njit(cache=True)
|
|
168
|
+
def _encode_body_njit(q_flat, band_starts, band_ends, scales_flat, out_buf):
|
|
169
|
+
bit_pos = 0
|
|
170
|
+
for fc in range(q_flat.shape[0]):
|
|
171
|
+
bit_pos = _encode_frame_channel(q_flat[fc], band_starts, band_ends, scales_flat[fc], out_buf, bit_pos)
|
|
172
|
+
return bit_pos
|
|
173
|
+
|
|
174
|
+
@njit(cache=True)
|
|
175
|
+
def _decode_body_njit(body_arr, q_flat, scales_flat, band_starts, band_ends):
|
|
176
|
+
bit_pos = 0
|
|
177
|
+
for fc in range(q_flat.shape[0]):
|
|
178
|
+
bit_pos = _decode_frame_channel(body_arr, bit_pos, q_flat[fc], scales_flat[fc], band_starts, band_ends)
|
|
179
|
+
return bit_pos
|
|
180
|
+
|
|
181
|
+
def _make_bark_bands(sr, n_coeffs, min_size=MIN_BAND_SIZE):
|
|
182
|
+
bark_hz = np.array([0, 100, 200, 300, 400, 510, 630, 770, 920, 1080, 1270, 1480, 1720, 2000, 2320, 2700, 3150, 3700, 4400, 5300, 6400, 7700, 9500, 12000, 15500, sr / 2], dtype=np.float64)
|
|
183
|
+
bin_centers = (np.arange(n_coeffs) + 0.5) * (sr / (2 * n_coeffs))
|
|
184
|
+
raw = []
|
|
185
|
+
for i in range(len(bark_hz) - 1):
|
|
186
|
+
start = int(np.searchsorted(bin_centers, bark_hz[i], side='left'))
|
|
187
|
+
end = int(np.searchsorted(bin_centers, bark_hz[i + 1], side='left'))
|
|
188
|
+
if end > start:
|
|
189
|
+
raw.append((start, end))
|
|
190
|
+
if raw:
|
|
191
|
+
if raw[0][0] > 0: raw[0] = (0, raw[0][1])
|
|
192
|
+
if raw[-1][1] < n_coeffs: raw[-1] = (raw[-1][0], n_coeffs)
|
|
193
|
+
merged = []
|
|
194
|
+
acc = None
|
|
195
|
+
for s, e in raw:
|
|
196
|
+
if acc is None:
|
|
197
|
+
acc = [s, e]
|
|
198
|
+
elif acc[1] - acc[0] < min_size:
|
|
199
|
+
acc[1] = e
|
|
200
|
+
else:
|
|
201
|
+
merged.append((acc[0], acc[1]))
|
|
202
|
+
acc = [s, e]
|
|
203
|
+
if acc is not None:
|
|
204
|
+
merged.append((acc[0], acc[1]))
|
|
205
|
+
return merged
|
|
206
|
+
|
|
207
|
+
def _ath_thresholds(sr, n_coeffs, offset_db=ATH_OFFSET_DB):
|
|
208
|
+
freqs = np.maximum(np.arange(n_coeffs) * sr / FRAME_SIZE, 20.0)
|
|
209
|
+
f_khz = freqs / 1000.0
|
|
210
|
+
ath_db = np.minimum(3.64 * f_khz ** (-0.8) - 6.5 * np.exp(-0.6 * (f_khz - 3.3) ** 2) + 1e-3 * f_khz ** 4, 75.0)
|
|
211
|
+
return (10.0 ** ((ath_db - offset_db) / 10.0)).astype(np.float64)
|
|
212
|
+
|
|
213
|
+
def _encode_body(coeffs, sr, delta, curve_bands, band_starts, band_ends, target_snr_db):
|
|
214
|
+
n_frames, _, ch = coeffs.shape
|
|
215
|
+
n_bands = len(band_starts)
|
|
216
|
+
|
|
217
|
+
ath_band = np.array([np.mean(_ath_thresholds(sr, N_COEFFS)[s:e]) for s, e in zip(band_starts, band_ends)])
|
|
218
|
+
|
|
219
|
+
energies = np.zeros((n_frames, n_bands, ch), dtype=np.float64)
|
|
220
|
+
for b in range(n_bands):
|
|
221
|
+
energies[:, b, :] = np.mean(coeffs[:, band_starts[b]:band_ends[b], :] ** 2, axis=1)
|
|
222
|
+
|
|
223
|
+
mask_smooth = energies.copy()
|
|
224
|
+
for shift in (1, 2, 3):
|
|
225
|
+
mask_smooth = np.maximum(mask_smooth, np.roll(energies, shift, axis=0))
|
|
226
|
+
mask_smooth = np.maximum(mask_smooth, np.roll(energies, -shift, axis=0))
|
|
227
|
+
|
|
228
|
+
spread = np.zeros_like(mask_smooth)
|
|
229
|
+
for b in range(n_bands):
|
|
230
|
+
acc = np.zeros((n_frames, ch), dtype=np.float64)
|
|
231
|
+
for db in range(-4, 5):
|
|
232
|
+
b2 = b + db
|
|
233
|
+
if 0 <= b2 < n_bands:
|
|
234
|
+
acc += mask_smooth[:, b2, :] * (1.0 - abs(db) / 5.0)
|
|
235
|
+
spread[:, b, :] = acc
|
|
236
|
+
|
|
237
|
+
mask = spread + ath_band[None, :, None]
|
|
238
|
+
base_deltas = delta * curve_bands.astype(np.float64)
|
|
239
|
+
delta_max = np.sqrt(mask / 20.0)
|
|
240
|
+
delta_floor = base_deltas[None, :, None] / np.geomspace(4.0, 0.5, n_bands)[None, :, None]
|
|
241
|
+
deltas = np.maximum(delta_floor, np.minimum(base_deltas[None, :, None], delta_max))
|
|
242
|
+
|
|
243
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
244
|
+
scale = np.round(2 * np.log2(deltas / base_deltas[None, :, None])).astype(np.int32)
|
|
245
|
+
np.clip(scale, SCALE_MIN, SCALE_MAX, out=scale)
|
|
246
|
+
deltas = base_deltas[None, :, None] * (2.0 ** (scale / 2.0))
|
|
247
|
+
|
|
248
|
+
quantized = np.zeros_like(coeffs, dtype=np.int32)
|
|
249
|
+
for b in range(n_bands):
|
|
250
|
+
d = deltas[:, b, :]
|
|
251
|
+
quantized[:, band_starts[b]:band_ends[b], :] = np.round(coeffs[:, band_starts[b]:band_ends[b], :] / d[:, None, :])
|
|
252
|
+
np.clip(quantized, -32767, 32767, out=quantized)
|
|
253
|
+
|
|
254
|
+
q_flat = np.ascontiguousarray(quantized.transpose(0, 2, 1).reshape(n_frames * ch, N_COEFFS))
|
|
255
|
+
scales_flat = np.ascontiguousarray(scale.transpose(0, 2, 1).reshape(n_frames * ch, n_bands))
|
|
256
|
+
|
|
257
|
+
out_buf = np.zeros(n_frames * ch * 3072, dtype=np.uint8)
|
|
258
|
+
bit_pos = _encode_body_njit(q_flat, band_starts, band_ends, scales_flat, out_buf)
|
|
259
|
+
return out_buf[:(bit_pos + 7) // 8].tobytes(), bit_pos
|
|
260
|
+
|
|
261
|
+
def encode_audio(pcm, sr, ch, delta, target_snr_db):
|
|
262
|
+
if pcm.ndim == 1:
|
|
263
|
+
pcm = pcm[:, None]
|
|
264
|
+
n_samples = pcm.shape[0]
|
|
265
|
+
n_frames = (n_samples + HOP - 1) // HOP + 1
|
|
266
|
+
|
|
267
|
+
pcm_padded = np.zeros(((n_frames + 1) * HOP, ch), dtype=np.float64)
|
|
268
|
+
pcm_padded[HOP:HOP + n_samples] = pcm
|
|
269
|
+
|
|
270
|
+
idx = np.arange(n_frames)[:, None] * HOP + np.arange(FRAME_SIZE)[None, :]
|
|
271
|
+
coeffs_L = (pcm_padded[idx, 0] * _WINDOW[None, :]) @ _MDCT_MATRIX
|
|
272
|
+
|
|
273
|
+
if ch == 2:
|
|
274
|
+
coeffs_R = (pcm_padded[idx, 1] * _WINDOW[None, :]) @ _MDCT_MATRIX
|
|
275
|
+
coeffs_lr = np.stack([coeffs_L, coeffs_R], axis=2)
|
|
276
|
+
coeffs_ms = np.stack([(coeffs_L + coeffs_R) * 0.5, (coeffs_L - coeffs_R) * 0.5], axis=2)
|
|
277
|
+
else:
|
|
278
|
+
coeffs_lr, coeffs_ms = coeffs_L[:, :, None], None
|
|
279
|
+
|
|
280
|
+
bark_bands = _make_bark_bands(sr, N_COEFFS)
|
|
281
|
+
band_starts = np.array([s for s, _ in bark_bands], dtype=np.int32)
|
|
282
|
+
band_ends = np.array([e for _, e in bark_bands], dtype=np.int32)
|
|
283
|
+
curve_bands = np.interp(np.linspace(0, 1, len(bark_bands)), np.linspace(0, 1, 8), DEFAULT_DELTA_CURVE).astype(np.float32)
|
|
284
|
+
|
|
285
|
+
body_lr, bits_lr = _encode_body(coeffs_lr, sr, delta, curve_bands, band_starts, band_ends, target_snr_db)
|
|
286
|
+
|
|
287
|
+
if ch == 2:
|
|
288
|
+
body_ms, bits_ms = _encode_body(coeffs_ms, sr, delta, curve_bands, band_starts, band_ends, target_snr_db)
|
|
289
|
+
use_ms, body = (1, body_ms) if bits_ms < bits_lr else (0, body_lr)
|
|
290
|
+
else:
|
|
291
|
+
use_ms, body = 0, body_lr
|
|
292
|
+
|
|
293
|
+
hdr = struct.pack(AUDIO_HDR_FMT, sr, ch, FRAME_SIZE, n_samples, float(delta), *[float(x) for x in DEFAULT_DELTA_CURVE], float(target_snr_db), use_ms, len(body))
|
|
294
|
+
return hdr + body
|
|
295
|
+
|
|
296
|
+
def decode_audio(payload):
|
|
297
|
+
(sr, ch, fs, n_samples, delta, c0, c1, c2, c3, c4, c5, c6, c7, target_snr_db, use_ms, body_len) = struct.unpack_from(AUDIO_HDR_FMT, payload, 0)
|
|
298
|
+
|
|
299
|
+
body = payload[AUDIO_HDR_SIZE:AUDIO_HDR_SIZE + body_len]
|
|
300
|
+
n_frames = (n_samples + HOP - 1) // HOP + 1
|
|
301
|
+
|
|
302
|
+
bark_bands = _make_bark_bands(sr, N_COEFFS)
|
|
303
|
+
band_starts = np.array([s for s, _ in bark_bands], dtype=np.int32)
|
|
304
|
+
band_ends = np.array([e for _, e in bark_bands], dtype=np.int32)
|
|
305
|
+
|
|
306
|
+
curve_orig = np.array([c0, c1, c2, c3, c4, c5, c6, c7], dtype=np.float32)
|
|
307
|
+
curve_bands = np.interp(np.linspace(0, 1, len(bark_bands)), np.linspace(0, 1, 8), curve_orig).astype(np.float32)
|
|
308
|
+
|
|
309
|
+
q_flat = np.zeros((n_frames * ch, N_COEFFS), dtype=np.int32)
|
|
310
|
+
scales_flat = np.zeros((n_frames * ch, len(bark_bands)), dtype=np.int32)
|
|
311
|
+
_decode_body_njit(np.frombuffer(body, dtype=np.uint8), q_flat, scales_flat, band_starts, band_ends)
|
|
312
|
+
|
|
313
|
+
coeffs = np.zeros((n_frames, N_COEFFS, ch), dtype=np.float64)
|
|
314
|
+
for b in range(len(bark_bands)):
|
|
315
|
+
d = delta * curve_bands[b] * (2.0 ** (scales_flat[:, b].reshape(n_frames, ch) / 2.0))
|
|
316
|
+
q_band = q_flat[:, band_starts[b]:band_ends[b]].reshape(n_frames, ch, band_ends[b] - band_starts[b]).transpose(0, 2, 1)
|
|
317
|
+
coeffs[:, band_starts[b]:band_ends[b], :] = q_band * d[:, None, :]
|
|
318
|
+
|
|
319
|
+
frames = (coeffs.transpose(0, 2, 1).reshape(-1, N_COEFFS) @ _IMDCT_MATRIX).reshape(n_frames, ch, FRAME_SIZE).transpose(0, 2, 1) * _WINDOW[None, :, None]
|
|
320
|
+
|
|
321
|
+
pcm_enc = np.zeros(((n_frames + 1) * HOP, ch), dtype=np.float64)
|
|
322
|
+
for f in range(n_frames):
|
|
323
|
+
pcm_enc[f * HOP : f * HOP + FRAME_SIZE] += frames[f]
|
|
324
|
+
|
|
325
|
+
pcm_padded = np.stack([pcm_enc[:, 0] + pcm_enc[:, 1], pcm_enc[:, 0] - pcm_enc[:, 1]], axis=1) if use_ms and ch == 2 else pcm_enc
|
|
326
|
+
return np.clip(pcm_padded[HOP:HOP + n_samples], -1.0, 1.0), sr, ch, n_samples
|
|
327
|
+
|
|
328
|
+
def get_audio_info(payload):
|
|
329
|
+
(sr, ch, _, n_samples, delta, _, _, _, _, _, _, _, _, target_snr_db, use_ms, body_len) = struct.unpack_from(AUDIO_HDR_FMT, payload, 0)
|
|
330
|
+
return dict(sr=sr, ch=ch, n_samples=n_samples, delta=delta, target_snr_db=target_snr_db, use_ms=use_ms, body_len=body_len)
|
core/container.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
|
|
3
|
+
MAGIC = b'LVTHN'
|
|
4
|
+
VERSION = 1
|
|
5
|
+
|
|
6
|
+
MEDIA_AUDIO = 0x01
|
|
7
|
+
MEDIA_IMAGE = 0x02
|
|
8
|
+
MEDIA_VIDEO = 0x03
|
|
9
|
+
|
|
10
|
+
HDR_FMT = '<5sHB'
|
|
11
|
+
HDR_SIZE = struct.calcsize(HDR_FMT)
|
|
12
|
+
|
|
13
|
+
def wrap(media_type, payload):
|
|
14
|
+
header = struct.pack(HDR_FMT, MAGIC, VERSION, media_type)
|
|
15
|
+
return header + payload
|
|
16
|
+
|
|
17
|
+
def unwrap(data):
|
|
18
|
+
if len(data) < HDR_SIZE:
|
|
19
|
+
raise ValueError('File too small')
|
|
20
|
+
magic, version, media_type = struct.unpack_from(HDR_FMT, data, 0)
|
|
21
|
+
if magic != MAGIC:
|
|
22
|
+
raise ValueError('Not a Leviathan file')
|
|
23
|
+
if version != VERSION:
|
|
24
|
+
raise ValueError(f'Unsupported version {version}')
|
|
25
|
+
return media_type, data[HDR_SIZE:]
|
core/image.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
import numpy as np
|
|
3
|
+
from scipy.fft import dctn, idctn
|
|
4
|
+
import zstandard as zstd
|
|
5
|
+
from numba import njit
|
|
6
|
+
|
|
7
|
+
IMG_HDR_FMT = '<IIBBf'
|
|
8
|
+
IMG_HDR_SIZE = struct.calcsize(IMG_HDR_FMT)
|
|
9
|
+
|
|
10
|
+
ZIGZAG_ORDER = np.array([
|
|
11
|
+
0, 1, 8, 16, 9, 2, 3, 10,
|
|
12
|
+
17, 24, 32, 25, 18, 11, 4, 5,
|
|
13
|
+
12, 19, 26, 33, 40, 48, 41, 34,
|
|
14
|
+
27, 20, 13, 6, 7, 14, 21, 28,
|
|
15
|
+
35, 42, 49, 56, 57, 50, 43, 36,
|
|
16
|
+
29, 22, 15, 23, 30, 37, 44, 51,
|
|
17
|
+
58, 59, 52, 45, 38, 31, 39, 46,
|
|
18
|
+
53, 60, 61, 54, 47, 55, 62, 63
|
|
19
|
+
], dtype=np.int32)
|
|
20
|
+
|
|
21
|
+
@njit(cache=True)
|
|
22
|
+
def _rgb_to_oklab_njit(rgb, out):
|
|
23
|
+
h, w = rgb.shape[0], rgb.shape[1]
|
|
24
|
+
for i in range(h):
|
|
25
|
+
for j in range(w):
|
|
26
|
+
r = rgb[i, j, 0] / 255.0
|
|
27
|
+
g = rgb[i, j, 1] / 255.0
|
|
28
|
+
b = rgb[i, j, 2] / 255.0
|
|
29
|
+
|
|
30
|
+
if r > 0.04045: r = ((r + 0.055) / 1.055) ** 2.4
|
|
31
|
+
else: r = r / 12.92
|
|
32
|
+
if g > 0.04045: g = ((g + 0.055) / 1.055) ** 2.4
|
|
33
|
+
else: g = g / 12.92
|
|
34
|
+
if b > 0.04045: b = ((b + 0.055) / 1.055) ** 2.4
|
|
35
|
+
else: b = b / 12.92
|
|
36
|
+
|
|
37
|
+
l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b
|
|
38
|
+
m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b
|
|
39
|
+
s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b
|
|
40
|
+
|
|
41
|
+
l = (l ** (1.0/3.0)) if l > 0 else 0.0
|
|
42
|
+
m = (m ** (1.0/3.0)) if m > 0 else 0.0
|
|
43
|
+
s = (s ** (1.0/3.0)) if s > 0 else 0.0
|
|
44
|
+
|
|
45
|
+
L = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s
|
|
46
|
+
a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s
|
|
47
|
+
bb = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s
|
|
48
|
+
|
|
49
|
+
out[i, j, 0] = L * 255.0
|
|
50
|
+
out[i, j, 1] = a * 255.0 + 128.0
|
|
51
|
+
out[i, j, 2] = bb * 255.0 + 128.0
|
|
52
|
+
|
|
53
|
+
@njit(cache=True)
|
|
54
|
+
def _oklab_to_rgb_njit(oklab, out):
|
|
55
|
+
h, w = oklab.shape[0], oklab.shape[1]
|
|
56
|
+
for i in range(h):
|
|
57
|
+
for j in range(w):
|
|
58
|
+
L = oklab[i, j, 0] / 255.0
|
|
59
|
+
a = (oklab[i, j, 1] - 128.0) / 255.0
|
|
60
|
+
b = (oklab[i, j, 2] - 128.0) / 255.0
|
|
61
|
+
|
|
62
|
+
l = L + 0.3963377774 * a + 0.2158037573 * b
|
|
63
|
+
m = L - 0.1055613458 * a - 0.0638541728 * b
|
|
64
|
+
s = L - 0.0894841775 * a - 1.2914855480 * b
|
|
65
|
+
|
|
66
|
+
l = l ** 3
|
|
67
|
+
m = m ** 3
|
|
68
|
+
s = s ** 3
|
|
69
|
+
|
|
70
|
+
r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s
|
|
71
|
+
g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s
|
|
72
|
+
bb = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
|
|
73
|
+
|
|
74
|
+
if r > 0.0031308: r = 1.055 * (r ** (1.0/2.4)) - 0.055
|
|
75
|
+
else: r = 12.92 * r
|
|
76
|
+
if g > 0.0031308: g = 1.055 * (g ** (1.0/2.4)) - 0.055
|
|
77
|
+
else: g = 12.92 * g
|
|
78
|
+
if bb > 0.0031308: bb = 1.055 * (bb ** (1.0/2.4)) - 0.055
|
|
79
|
+
else: bb = 12.92 * bb
|
|
80
|
+
|
|
81
|
+
r = max(0.0, min(1.0, r))
|
|
82
|
+
g = max(0.0, min(1.0, g))
|
|
83
|
+
bb = max(0.0, min(1.0, bb))
|
|
84
|
+
|
|
85
|
+
out[i, j, 0] = int(r * 255.0 + 0.5)
|
|
86
|
+
out[i, j, 1] = int(g * 255.0 + 0.5)
|
|
87
|
+
out[i, j, 2] = int(bb * 255.0 + 0.5)
|
|
88
|
+
|
|
89
|
+
@njit(cache=True)
|
|
90
|
+
def _rle_encode_all_fast(zigzag_blocks, out_pairs_1d):
|
|
91
|
+
n_blocks = zigzag_blocks.shape[0]
|
|
92
|
+
out_pos = 0
|
|
93
|
+
for b in range(n_blocks):
|
|
94
|
+
run = 0
|
|
95
|
+
for k in range(64):
|
|
96
|
+
v = zigzag_blocks[b, k]
|
|
97
|
+
if v == 0:
|
|
98
|
+
run += 1
|
|
99
|
+
else:
|
|
100
|
+
while run > 15:
|
|
101
|
+
out_pairs_1d[out_pos] = 15
|
|
102
|
+
out_pairs_1d[out_pos+1] = 0
|
|
103
|
+
out_pos += 2
|
|
104
|
+
run -= 16
|
|
105
|
+
out_pairs_1d[out_pos] = run
|
|
106
|
+
out_pairs_1d[out_pos+1] = v
|
|
107
|
+
out_pos += 2
|
|
108
|
+
run = 0
|
|
109
|
+
if run > 0:
|
|
110
|
+
out_pairs_1d[out_pos] = 0
|
|
111
|
+
out_pairs_1d[out_pos+1] = 0
|
|
112
|
+
out_pos += 2
|
|
113
|
+
return out_pos // 2
|
|
114
|
+
|
|
115
|
+
@njit(cache=True)
|
|
116
|
+
def _rle_decode_all_fast(pairs_1d, n_pairs, n_blocks, out_zigzag):
|
|
117
|
+
idx = 0
|
|
118
|
+
for b in range(n_blocks):
|
|
119
|
+
for k in range(64):
|
|
120
|
+
out_zigzag[b, k] = 0
|
|
121
|
+
pos = 0
|
|
122
|
+
while pos < 64 and idx < n_pairs:
|
|
123
|
+
run = pairs_1d[idx*2]
|
|
124
|
+
val = pairs_1d[idx*2+1]
|
|
125
|
+
idx += 1
|
|
126
|
+
if val == 0 and run == 0:
|
|
127
|
+
break
|
|
128
|
+
pos += run
|
|
129
|
+
if pos < 64:
|
|
130
|
+
out_zigzag[b, pos] = val
|
|
131
|
+
pos += 1
|
|
132
|
+
return idx
|
|
133
|
+
|
|
134
|
+
def _get_q_matrix(quality, is_chroma):
|
|
135
|
+
q = np.clip(quality, 1, 100)
|
|
136
|
+
step = (100 - q) / 2.0 + 1.0
|
|
137
|
+
if is_chroma:
|
|
138
|
+
step *= 1.5
|
|
139
|
+
return np.ones((8, 8), dtype=np.float32) * step
|
|
140
|
+
|
|
141
|
+
def _encode_channel_fast(channel, q_matrix):
|
|
142
|
+
h, w = channel.shape
|
|
143
|
+
pad_h = (8 - h % 8) % 8
|
|
144
|
+
pad_w = (8 - w % 8) % 8
|
|
145
|
+
padded = np.pad(channel, ((0, pad_h), (0, pad_w)), mode='edge') if (pad_h or pad_w) else channel
|
|
146
|
+
|
|
147
|
+
h_blocks = padded.shape[0] // 8
|
|
148
|
+
w_blocks = padded.shape[1] // 8
|
|
149
|
+
|
|
150
|
+
blocks = np.ascontiguousarray(padded.reshape(h_blocks, 8, w_blocks, 8).transpose(0, 2, 1, 3))
|
|
151
|
+
dct_blocks = dctn(blocks, type=2, norm='ortho', axes=(-2, -1))
|
|
152
|
+
quantized = np.round(dct_blocks / q_matrix).astype(np.int32)
|
|
153
|
+
|
|
154
|
+
flat = quantized.reshape(-1, 64)
|
|
155
|
+
zigzag = np.ascontiguousarray(flat[:, ZIGZAG_ORDER])
|
|
156
|
+
|
|
157
|
+
n_blocks = zigzag.shape[0]
|
|
158
|
+
max_pairs = n_blocks * 65
|
|
159
|
+
out_pairs = np.zeros(max_pairs * 2, dtype=np.int32)
|
|
160
|
+
n_pairs = _rle_encode_all_fast(zigzag, out_pairs)
|
|
161
|
+
|
|
162
|
+
return out_pairs[:n_pairs * 2]
|
|
163
|
+
|
|
164
|
+
def _decode_channel_fast(pairs_1d, q_matrix, h, w):
|
|
165
|
+
pad_h = (8 - h % 8) % 8
|
|
166
|
+
pad_w = (8 - w % 8) % 8
|
|
167
|
+
h_blocks = (h + pad_h) // 8
|
|
168
|
+
w_blocks = (w + pad_w) // 8
|
|
169
|
+
n_blocks = h_blocks * w_blocks
|
|
170
|
+
|
|
171
|
+
zigzag = np.zeros((n_blocks, 64), dtype=np.int32)
|
|
172
|
+
n_pairs = len(pairs_1d) // 2
|
|
173
|
+
_rle_decode_all_fast(pairs_1d, n_pairs, n_blocks, zigzag)
|
|
174
|
+
|
|
175
|
+
flat = np.zeros((n_blocks, 64), dtype=np.int32)
|
|
176
|
+
flat[:, ZIGZAG_ORDER] = zigzag
|
|
177
|
+
|
|
178
|
+
blocks = flat.reshape(h_blocks, w_blocks, 8, 8).astype(np.float32) * q_matrix
|
|
179
|
+
spatial_blocks = idctn(blocks, type=2, norm='ortho', axes=(-2, -1))
|
|
180
|
+
|
|
181
|
+
padded = spatial_blocks.transpose(0, 2, 1, 3).reshape(h_blocks * 8, w_blocks * 8)
|
|
182
|
+
return padded[:h, :w]
|
|
183
|
+
|
|
184
|
+
def encode_image(pixels, width, height, channels=3, quality=90, level=19):
|
|
185
|
+
has_alpha = (channels == 4)
|
|
186
|
+
flags = 1 if has_alpha else 0
|
|
187
|
+
|
|
188
|
+
if has_alpha:
|
|
189
|
+
rgb_pixels = pixels[:, :, :3]
|
|
190
|
+
alpha_channel = pixels[:, :, 3].astype(np.float32)
|
|
191
|
+
else:
|
|
192
|
+
rgb_pixels = pixels
|
|
193
|
+
|
|
194
|
+
oklab = np.zeros((height, width, 3), dtype=np.float32)
|
|
195
|
+
_rgb_to_oklab_njit(rgb_pixels, oklab)
|
|
196
|
+
|
|
197
|
+
q_luma = _get_q_matrix(quality, is_chroma=False)
|
|
198
|
+
q_chroma = _get_q_matrix(quality, is_chroma=True)
|
|
199
|
+
q_alpha = _get_q_matrix(quality, is_chroma=False)
|
|
200
|
+
|
|
201
|
+
raw_pairs = []
|
|
202
|
+
raw_pairs.append(_encode_channel_fast(oklab[:, :, 0], q_luma))
|
|
203
|
+
raw_pairs.append(_encode_channel_fast(oklab[:, :, 1], q_chroma))
|
|
204
|
+
raw_pairs.append(_encode_channel_fast(oklab[:, :, 2], q_chroma))
|
|
205
|
+
if has_alpha:
|
|
206
|
+
raw_pairs.append(_encode_channel_fast(alpha_channel, q_alpha))
|
|
207
|
+
|
|
208
|
+
compressed_channels = []
|
|
209
|
+
cctx = zstd.ZstdCompressor(level=level, threads=-1, write_content_size=True)
|
|
210
|
+
for pairs in raw_pairs:
|
|
211
|
+
compressed_channels.append(cctx.compress(pairs.tobytes()))
|
|
212
|
+
|
|
213
|
+
comp_lens = [len(c) for c in compressed_channels]
|
|
214
|
+
|
|
215
|
+
header = struct.pack(IMG_HDR_FMT, width, height, channels, flags, float(quality))
|
|
216
|
+
meta = struct.pack(f'<{channels}I', *comp_lens)
|
|
217
|
+
|
|
218
|
+
body = b''.join(compressed_channels)
|
|
219
|
+
return header + meta + body
|
|
220
|
+
|
|
221
|
+
def decode_image(payload):
|
|
222
|
+
if len(payload) < IMG_HDR_SIZE:
|
|
223
|
+
raise ValueError("Image payload too small")
|
|
224
|
+
|
|
225
|
+
width, height, channels, flags, quality = struct.unpack_from(IMG_HDR_FMT, payload, 0)
|
|
226
|
+
has_alpha = bool(flags & 1)
|
|
227
|
+
|
|
228
|
+
pos = IMG_HDR_SIZE
|
|
229
|
+
comp_lens = struct.unpack_from(f'<{channels}I', payload, pos)
|
|
230
|
+
pos += channels * 4
|
|
231
|
+
|
|
232
|
+
compressed_channels = []
|
|
233
|
+
for l in comp_lens:
|
|
234
|
+
compressed_channels.append(payload[pos:pos+l])
|
|
235
|
+
pos += l
|
|
236
|
+
|
|
237
|
+
raw_pairs = []
|
|
238
|
+
dctx = zstd.ZstdDecompressor()
|
|
239
|
+
for comp in compressed_channels:
|
|
240
|
+
packed = dctx.decompress(comp, max_output_size=width*height*64*4)
|
|
241
|
+
raw_pairs.append(np.frombuffer(packed, dtype='<i4').astype(np.int32))
|
|
242
|
+
|
|
243
|
+
q_luma = _get_q_matrix(quality, is_chroma=False)
|
|
244
|
+
q_chroma = _get_q_matrix(quality, is_chroma=True)
|
|
245
|
+
q_alpha = _get_q_matrix(quality, is_chroma=False)
|
|
246
|
+
|
|
247
|
+
oklab = np.zeros((height, width, 3), dtype=np.float32)
|
|
248
|
+
oklab[:, :, 0] = _decode_channel_fast(raw_pairs[0], q_luma, height, width)
|
|
249
|
+
oklab[:, :, 1] = _decode_channel_fast(raw_pairs[1], q_chroma, height, width)
|
|
250
|
+
oklab[:, :, 2] = _decode_channel_fast(raw_pairs[2], q_chroma, height, width)
|
|
251
|
+
|
|
252
|
+
rgb = np.zeros((height, width, 3), dtype=np.uint8)
|
|
253
|
+
_oklab_to_rgb_njit(oklab, rgb)
|
|
254
|
+
|
|
255
|
+
if has_alpha:
|
|
256
|
+
alpha_channel = _decode_channel_fast(raw_pairs[3], q_alpha, height, width)
|
|
257
|
+
alpha = np.clip(alpha_channel, 0, 255).astype(np.uint8)
|
|
258
|
+
return np.dstack((rgb, alpha))
|
|
259
|
+
|
|
260
|
+
return rgb
|
|
261
|
+
|
|
262
|
+
def get_image_info(payload):
|
|
263
|
+
width, height, channels, flags, quality = struct.unpack_from(IMG_HDR_FMT, payload, 0)
|
|
264
|
+
return dict(
|
|
265
|
+
width=width, height=height, channels=channels,
|
|
266
|
+
has_alpha=bool(flags & 1), quality=quality
|
|
267
|
+
)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lvthn-codec
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Universal LVTHN media codec: audio (MDCT + Bark + Psy) and images (Oklab + DCT + zstd)
|
|
5
|
+
Author: FreshGame
|
|
6
|
+
Maintainer: FreshLend Studio
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: audio,codec,image,compression,mdct,dct,psychoacoustics,oklab,lvn,patent-free
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
16
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
License-File: PATENTS
|
|
21
|
+
Requires-Dist: numpy==2.2.6
|
|
22
|
+
Requires-Dist: scipy==1.15.3
|
|
23
|
+
Requires-Dist: numba==0.68.1
|
|
24
|
+
Requires-Dist: zstandard==0.25.0
|
|
25
|
+
Requires-Dist: Pillow==12.3.0
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# LVTHN-CODEC [RU](README.md) | **EN**
|
|
29
|
+
|
|
30
|
+
A modular, patent-clean, and highly efficient media codec written in Python.
|
|
31
|
+
It uses a universal container (`.lvn`) that supports both **Audio** and **Images**, applying modern psychoacoustic and psychovisual models to outperform industry standards like MP3 and PNG.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
* **Universal container (`.lvn`):** One format for everything. The decoder automatically detects what is inside.
|
|
36
|
+
* **Audio codec:** MDCT + Mid/Side stereo + Bark scale bands + Absolute Threshold of Hearing (ATH) + Adaptive quantization.
|
|
37
|
+
* **Image codec:** Oklab color space + Block DCT + RLE + Zstandard.
|
|
38
|
+
* **Performance:**
|
|
39
|
+
* Audio outperforms MP3 in size by **~15%** at the same perceived quality.
|
|
40
|
+
* Images outperform PNG in size by **6 to 7 times** with visually indistinguishable quality.
|
|
41
|
+
* High speed: Audio RTF < 0.02, 4K image decoding takes about 3 seconds.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
Ensure you have Python 3.10+ installed.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install numpy scipy numba zstandard Pillow sounddevice PySide6
|
|
49
|
+
```
|
|
50
|
+
*(Note: For `converter.py` to work with formats like MP3 or JPG, you need to install `ffmpeg` and add it to the system PATH environment variable).*
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### 1. Codec CLI (`codec.py`)
|
|
55
|
+
Direct encoding and decoding to and from the `.lvn` format.
|
|
56
|
+
```bash
|
|
57
|
+
# Audio
|
|
58
|
+
python codec.py encode track.wav track.lvn compact
|
|
59
|
+
python codec.py decode track.lvn out.wav
|
|
60
|
+
|
|
61
|
+
# Images
|
|
62
|
+
python codec.py encode-img image.png image.lvn fast
|
|
63
|
+
python codec.py decode-img image.lvn out.png
|
|
64
|
+
|
|
65
|
+
# File info
|
|
66
|
+
python codec.py info track.lvn
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2. Converter (`converter.py`)
|
|
70
|
+
Converts any supported format (via ffmpeg) to `.lvn` and back.
|
|
71
|
+
```bash
|
|
72
|
+
python converter.py to-lvn track.mp3 track.lvn --profile default
|
|
73
|
+
python converter.py from-lvn track.lvn track_out.mp3 --bitrate 192k
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 3. Player (`player.py`)
|
|
77
|
+
Modern GUI player and image viewer based on PySide6.
|
|
78
|
+
```bash
|
|
79
|
+
python player.py track.lvn
|
|
80
|
+
python player.py image.lvn
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 4. Benchmark (`benchmark.py`)
|
|
84
|
+
Testing speed and compression ratio on your files.
|
|
85
|
+
```bash
|
|
86
|
+
python benchmark.py --audio audio.wav --image image.png --image-profiles fast max
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Profiles
|
|
90
|
+
|
|
91
|
+
### Audio Profiles
|
|
92
|
+
| Profile | Description | SNR | Best for |
|
|
93
|
+
|---|---|---|---|
|
|
94
|
+
| `compact` | Maximum compression | 20.0 | Everyday listening, saving space |
|
|
95
|
+
| `default` | Balanced | 24.0 | High-quality music |
|
|
96
|
+
| `speech` | Voice optimized | 20.0 | Podcasts, audiobooks |
|
|
97
|
+
| `detail` | Preserves quiet details | 24.0 | Lo-Fi, acoustic, vinyl |
|
|
98
|
+
| `master` | Archival quality | 30.0 | Mastering, studio archives |
|
|
99
|
+
|
|
100
|
+
### Image Profiles
|
|
101
|
+
| Profile | Quality | Zstd Level | Best for |
|
|
102
|
+
|---|---|---|---|
|
|
103
|
+
| `fast` | 85 | 1 | Quick preview, game textures |
|
|
104
|
+
| `default` | 92 | 12 | Web images, everyday use |
|
|
105
|
+
| `max` | 98 | 19 | Archiving, professional photography |
|
|
106
|
+
|
|
107
|
+
## Benchmarks
|
|
108
|
+
|
|
109
|
+
### Audio (179 sec track)
|
|
110
|
+
| File | Profile | Bitrate | SNR | Enc RTF | Dec RTF | Compression |
|
|
111
|
+
|---|---|---|---|---|---|---|
|
|
112
|
+
| acoustic.wav | compact | 113 kbps | 27.5 dB | 0.022 | 0.009 | 13.5x |
|
|
113
|
+
| acoustic.wav | master | 225 kbps | 40.8 dB | 0.020 | 0.010 | 6.8x |
|
|
114
|
+
|
|
115
|
+
### Images (4K 8.29 MP)
|
|
116
|
+
| File | Profile | PSNR | Enc Time | Dec Time | Compression |
|
|
117
|
+
|---|---|---|---|---|---|
|
|
118
|
+
| 4k_image.png | fast | 42.1 dB | 4.10s | 3.14s | 7.1x |
|
|
119
|
+
| 4k_image.png | max | 48.3 dB | 29.32s | 3.14s | 4.3x |
|
|
120
|
+
|
|
121
|
+
*(PSNR > 40 dB is considered visually indistinguishable from the original. RTF < 1.0 means faster than real time).*
|
|
122
|
+
|
|
123
|
+
## Architecture
|
|
124
|
+
|
|
125
|
+
* `codec.py` - Main CLI and API.
|
|
126
|
+
* `core/container.py` - Universal LVTHN wrapper.
|
|
127
|
+
* `core/audio.py` - Audio math (MDCT, Bark, Psy).
|
|
128
|
+
* `core/image.py` - Image math (Oklab, DCT).
|
|
129
|
+
* `player.py` - GUI (PySide6).
|
|
130
|
+
* `converter.py` - FFmpeg wrapper.
|
|
131
|
+
* `benchmark.py` - Performance testing tool.
|
|
132
|
+
|
|
133
|
+
## License and Patents
|
|
134
|
+
|
|
135
|
+
The project is distributed under the **MIT** license.
|
|
136
|
+
It is built exclusively on patent-free algorithms (MDCT, Oklab, DCT, Rice coding, Zstandard). See the `PATENTS` file for a detailed breakdown.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
codec.py,sha256=qBczZSDoOPzLqAMz49N1lIM6PHV7GBkhh_qaBBp1s3c,6001
|
|
2
|
+
core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
core/audio.py,sha256=5CBFQAiHg9zUQRjVkMbf9RcWzEYXmAoblXdlsq7lkYM,13277
|
|
4
|
+
core/container.py,sha256=tis5Y-BpfwwAcmD5_HlXI6NQJ98UhvMUY8QalO0GxUc,660
|
|
5
|
+
core/image.py,sha256=LgVaA_VCtEoEpVvDg8mtmlkiSQFKHydxBuZSQBVtJjQ,9684
|
|
6
|
+
lvthn_codec-1.0.0.dist-info/licenses/LICENSE,sha256=oydlZ-tVSj8Dhk5n6xZTAesffEyD4P5mZCW0-gpOaKQ,1092
|
|
7
|
+
lvthn_codec-1.0.0.dist-info/licenses/PATENTS,sha256=9Y191lC6dM6RvaraUMc8nust5l-j9V_tyzs793OslRQ,1300
|
|
8
|
+
lvthn_codec-1.0.0.dist-info/METADATA,sha256=xQGTOE82E1TeHKkGsrdfEvweebdDxtduC8gter98IEQ,5091
|
|
9
|
+
lvthn_codec-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
10
|
+
lvthn_codec-1.0.0.dist-info/top_level.txt,sha256=ns1lgx8tWumjOWKwOow_jNeT-5onvR86t_2TJLGw8zo,11
|
|
11
|
+
lvthn_codec-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FreshLend Studio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
LVTHN-CODEC — Patent Notice
|
|
2
|
+
|
|
3
|
+
This software is released under the MIT License.
|
|
4
|
+
|
|
5
|
+
The author is not aware of any patents that cover the algorithms implemented
|
|
6
|
+
here. The core techniques used are:
|
|
7
|
+
|
|
8
|
+
Audio:
|
|
9
|
+
- MDCT / IMDCT with sine window (Princen-Bradley, 1986–87; expired)
|
|
10
|
+
- TDAC (Time-Domain Aliasing Cancellation, 1986; expired)
|
|
11
|
+
- Mid/Side stereo coding (Blumlein, 1930s; classic technique)
|
|
12
|
+
- Bark-scale band partitioning (Barkhausen 1960s, public domain)
|
|
13
|
+
- Absolute Threshold of Hearing (Terhardt 1979, public domain)
|
|
14
|
+
- Rice coding (Golomb 1966, Rice 1979; expired)
|
|
15
|
+
- Adaptive per-band quantization (basic idea, expired)
|
|
16
|
+
|
|
17
|
+
Images:
|
|
18
|
+
- Oklab color space (Björn Ottosson, 2020; public domain)
|
|
19
|
+
- Block DCT (discrete cosine transform; expired)
|
|
20
|
+
- Run-Length Encoding (RLE; public domain)
|
|
21
|
+
- Zstandard compression (Yann Collet, 2015; BSD/GPLv2, patent-free)
|
|
22
|
+
|
|
23
|
+
This is NOT a legal guarantee of patent non-infringement. No warranty is
|
|
24
|
+
provided. If you distribute binaries based on this code commercially,
|
|
25
|
+
consult a patent attorney about your jurisdiction.
|
|
26
|
+
|
|
27
|
+
The author has intentionally avoided:
|
|
28
|
+
- Full psychoacoustic masking models identical to MP3/AAC
|
|
29
|
+
- SBR / PS / HE-AAC extensions
|
|
30
|
+
- Any code from FFmpeg, LAME, Opus, Vorbis, or other existing codecs
|