filejack 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.
@@ -0,0 +1,231 @@
1
+ import queue
2
+ import threading
3
+ import time
4
+ import zlib
5
+ from collections import deque
6
+ from concurrent.futures.thread import ThreadPoolExecutor
7
+ from dataclasses import dataclass
8
+ from fractions import Fraction
9
+ from statistics import median
10
+
11
+ from reedsolo import ReedSolomonError
12
+ from scipy.signal import hilbert, butter, sosfiltfilt, resample_poly
13
+
14
+ from scipy.io import wavfile
15
+
16
+ from .conversions import *
17
+ from .decode_frames import decode_frames
18
+ from .values import *
19
+ from .RSC import *
20
+ import numpy as np
21
+ # import sounddevice as sd
22
+
23
+
24
+ @dataclass
25
+ class Chunk:
26
+ stream_id: str
27
+ start_abs: int # absolute sample index for this source stream
28
+ samples: np.ndarray # float32
29
+
30
+ class SourceBuffer:
31
+ def __init__(self, overlap: int):
32
+ self.total = 0
33
+ self.parts = deque()
34
+ self.overlap = overlap
35
+
36
+ def append(self, x: np.ndarray):
37
+ x = np.asarray(x, dtype=np.float32)
38
+
39
+ self.parts.append(x)
40
+ self.total += len(x)
41
+
42
+ def available_abs_end(self) -> int:
43
+ return self.total - 2 * self.overlap
44
+
45
+ def pop_slice(self, size) -> np.ndarray:
46
+ abs_start = 0
47
+ abs_end = size + self.overlap * 2
48
+
49
+ need = abs_end - abs_start
50
+
51
+ if need > self.total:
52
+ raise IndexError("slice not available yet")
53
+
54
+ out = np.empty(need, dtype=np.float32)
55
+ w = 0
56
+
57
+ while self.parts and w < need:
58
+ if len(self.parts[0]) <= need - w - 2 * self.overlap:
59
+ seg = self.parts.popleft()
60
+ out[w:w+len(seg)] = seg
61
+ w += len(seg)
62
+ else:
63
+ seg = self.parts[0][:need - w]
64
+ out[w:w+len(seg)] = seg
65
+ self.parts[0] = self.parts[0][need - w - 2 * self.overlap:]
66
+ w += len(seg)
67
+
68
+ self.total -= need - 2 * self.overlap
69
+ return out
70
+
71
+
72
+ class AsyncDecodeManager:
73
+ def __init__(self, block_sec=3.0, overlap_sec=0.1):
74
+ self.block_samples_len = int(round(SAMPLE_RATE * block_sec))
75
+ self.overlap_samples_len = int(round(SAMPLE_RATE * overlap_sec))
76
+ self.buffer = np.array([], dtype=np.float32)
77
+ self.frames_payload = {}
78
+ self.total_expected = None
79
+ self.lock = threading.Lock()
80
+ self.rsc_error = 0
81
+ self.header_error = 0
82
+ self.crc_error = 0
83
+ self.duplicate_frames = 0
84
+
85
+ self.no_lock = 0
86
+ self.rsc_error = 0
87
+ self.header_error = 0
88
+ self.crc_error = 0
89
+
90
+ self.buffers: dict[str, SourceBuffer] = {}
91
+ self.next_block_start: dict[str, int] = {}
92
+
93
+ self.queue = queue.Queue(maxsize=2000)
94
+ self.executor = ThreadPoolExecutor(max_workers=32)
95
+ self.limit = threading.BoundedSemaphore(32)
96
+
97
+ self._stop = threading.Event()
98
+ self._thread = threading.Thread(target=self._run, daemon=True)
99
+
100
+ self.max_keep = 5 * self.block_samples_len + 3 * self.overlap_samples_len
101
+
102
+ def start(self):
103
+
104
+ self._stop = threading.Event()
105
+ self._thread.start()
106
+
107
+ def stop(self):
108
+ self._stop.set()
109
+ self._thread.join()
110
+ self.executor.shutdown(wait=True)
111
+
112
+ def push_chunk(self, chunk):
113
+ # If you have sources with different fs: resample here to target_fs (not shown)
114
+ self.queue.put(chunk)
115
+
116
+ def _ensure_stream(self, stream_id: str):
117
+ if stream_id not in self.buffers:
118
+ self.buffers[stream_id] = SourceBuffer(overlap=self.overlap_samples_len)
119
+ self.next_block_start[stream_id] = 0
120
+
121
+ def _run(self):
122
+ while not self._stop.is_set():
123
+ try:
124
+ chunk = self.queue.get(timeout=0.1)
125
+ except queue.Empty:
126
+ time.sleep(0.1)
127
+ continue
128
+
129
+ try:
130
+ self._ensure_stream(chunk.stream_id)
131
+ sb = self.buffers[chunk.stream_id]
132
+ sb.append(chunk.samples)
133
+ self._schedule_ready(chunk.stream_id)
134
+ finally:
135
+ self.queue.task_done()
136
+
137
+ def _schedule_ready(self, stream_id: str):
138
+ sb = self.buffers[stream_id]
139
+ next = self.next_block_start[stream_id]
140
+
141
+ while True:
142
+ start = max(0, next - self.overlap_samples_len)
143
+ end = start + self.block_samples_len + 2 * self.overlap_samples_len
144
+
145
+ # start = max(0, next)
146
+ # end = start + self.block_samples_len
147
+
148
+ if sb.available_abs_end() < self.block_samples_len:
149
+ break # not enough samples yet
150
+
151
+ rx_block = sb.pop_slice(self.block_samples_len)
152
+
153
+ # submit decode job
154
+ # self.limit.acquire()
155
+ fut = self.executor.submit(
156
+ self._decode_one,
157
+ rx_block, stream_id, start, end
158
+ )
159
+ fut.add_done_callback(self._merge_result)
160
+
161
+ # advance to next valid block (no overlap in valid regions)
162
+ next += self.block_samples_len
163
+ self.next_block_start[stream_id] = next
164
+
165
+ def _decode_one(self, rx_block, stream_id: str, start, end):
166
+ frames_block, total_expected, ok_log, no_lock, rsc_error, header_error, crc_error = decode_frames(
167
+ rx_block, SAMPLE_RATE,
168
+ search_step=SAMPLES_PER_SYMBOL, quick=True
169
+ )
170
+ return (frames_block, total_expected, ok_log, no_lock, rsc_error, header_error, crc_error, stream_id, start, end)
171
+
172
+ def _merge_result(self, fut):
173
+ # self.limit.release()
174
+ payloads, total, ok_log, no_lock, rsc_error, header_error, crc_error, stream_id, start, end = fut.result()
175
+ with self.lock:
176
+ if total is not None and self.total_expected is None:
177
+ self.total_expected = total
178
+
179
+ for seq, payload in payloads.items():
180
+ self.no_lock += no_lock
181
+ self.rsc_error += rsc_error
182
+ self.header_error += header_error
183
+ self.crc_error += crc_error
184
+
185
+ if seq not in self.frames_payload:
186
+ self.frames_payload[seq] = payload
187
+ else:
188
+ self.duplicate_frames += 1
189
+
190
+ print(f"\rBlock(Sample): {end // self.block_samples_len}/{self.buffers[stream_id].available_abs_end() // self.block_samples_len}({end}/{self.buffers[stream_id].available_abs_end()}) Frames: {len(self.frames_payload)}(+{self.duplicate_frames})/{self.total_expected} no_lock: {self.no_lock} rsc_error: {self.rsc_error} header_error: {self.header_error} crc_error: {self.crc_error}", end='')
191
+
192
+
193
+ def feed_wav(manager: AsyncDecodeManager, stream_id: str, path: str, chunk_size=16000):
194
+ fs, rx = wavfile.read(path)
195
+ rx = rx.astype(np.float32)
196
+ pos = 0
197
+ while pos < len(rx):
198
+ x = rx[pos:pos+chunk_size]
199
+ manager.push_chunk(Chunk(stream_id=stream_id, start_abs=pos, samples=x))
200
+ pos += len(x)
201
+ manager.push_chunk(Chunk(stream_id, start_abs=pos, samples=np.zeros((manager.block_samples_len + manager.overlap_samples_len,), dtype=np.float32)))
202
+
203
+
204
+ if __name__ == '__main__':
205
+ file = "bmp"
206
+
207
+ manager = AsyncDecodeManager()
208
+ manager.start()
209
+
210
+ # sample_pos = 0
211
+ # def callback(indata, frames, time_info, status):
212
+ # if status:
213
+ # print(status, flush=True)
214
+ #
215
+ # global sample_pos
216
+ # manager.push_chunk(Chunk(stream_id="mic", start_abs=sample_pos, samples=indata[:, 0].copy()))
217
+ # sample_pos += indata[:, 0].shape[0]
218
+ #
219
+ # stream = sd.InputStream(samplerate=SAMPLE_RATE, channels=1, dtype='float32', blocksize=4096, callback=callback)
220
+ #
221
+ # stream.start()
222
+ #
223
+ # while True:
224
+ # time.sleep(1)
225
+
226
+ feed_wav(manager, "wav", f"{file}.wav")
227
+ print("read wav")
228
+ # feed_wav(manager, "wav1", f"{file}1.wav")
229
+
230
+ manager.queue.join()
231
+ manager.stop()
filejack/RSC.py ADDED
@@ -0,0 +1,50 @@
1
+ import struct
2
+ from reedsolo import RSCodec
3
+
4
+ from .values import *
5
+
6
+
7
+ N = 255
8
+ NSYM = 32
9
+ K = N - NSYM
10
+ RSC = RSCodec(NSYM)
11
+
12
+ HDR_FMT = ">IIH"
13
+ HDR_LEN = struct.calcsize(HDR_FMT)
14
+
15
+ CRC_LEN = 4
16
+
17
+ MAX_PAYLOAD = K - HDR_LEN - CRC_LEN
18
+
19
+
20
+ def lfsr_bits(n: int, seed: int, taps=(7, 6)) -> list[int]:
21
+ """
22
+ Simple LFSR bit generator.
23
+ taps are 1-indexed bit positions within the register size (max(taps)).
24
+ """
25
+ m = max(taps)
26
+ mask = (1 << m) - 1
27
+ reg = seed & mask
28
+ if reg == 0:
29
+ reg = 1 # avoid stuck-at-zero
30
+
31
+ out = []
32
+ for _ in range(n):
33
+ # output bit (LSB)
34
+ out.append(reg & 1)
35
+
36
+ # feedback = XOR of tap bits
37
+ fb = 0
38
+ for t in taps:
39
+ fb ^= (reg >> (t - 1)) & 1
40
+
41
+ # shift right, insert feedback at MSB
42
+ reg = (reg >> 1) | (fb << (m - 1))
43
+ reg &= mask
44
+ return out
45
+
46
+ def bits_to_steps(bits01: list[int]) -> list[int]:
47
+ return [PSK // 2 if b else 0 for b in bits01]
48
+
49
+ PREAMBLE_STEPS = bits_to_steps(lfsr_bits(PREAMBLE_SYMS, seed=0x5D, taps=(7, 6)))
50
+ SYNCWORD_STEPS = bits_to_steps(lfsr_bits(SYNCWORD_SYMS, seed=0x6B, taps=(7, 6)))
filejack/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from .conversions import bytes_to_symbols, symbols_to_bytes
2
+ from .encode_frames import build_frames, encode_frames, encode_data
3
+ from .decode_frames import decode_frames
4
+ from .decode_data import decode_data
5
+ from .merge_frames import merge_frames, save_fjf, load_fjf
6
+ from .reconstruct_data import reconstruct_data
filejack/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+
4
+ raise SystemExit(main())
filejack/cli.py ADDED
@@ -0,0 +1,96 @@
1
+ import argparse
2
+ import os
3
+ import zlib
4
+
5
+ import numpy as np
6
+ from scipy.io import wavfile
7
+
8
+ from .decode_data import decode_data
9
+ from .encode_frames import encode_data
10
+ from .merge_frames import save_fjf
11
+ from .reconstruct_data import reconstruct_data
12
+ from .values import *
13
+
14
+
15
+ def encode_command(args) -> int:
16
+ output = args.output or args.input + ".wav"
17
+
18
+ data = open(args.input, 'rb').read()
19
+
20
+ waveform = encode_data(data)
21
+
22
+ if args.mono:
23
+ wavfile.write(output, SAMPLE_RATE, waveform)
24
+ else:
25
+ stereo = np.column_stack([waveform, -waveform])
26
+ wavfile.write(output, SAMPLE_RATE, stereo)
27
+
28
+ print(f"Encoded {len(data)} bytes into {output}")
29
+ print("Input CRC32:", zlib.crc32(data))
30
+ return 0
31
+
32
+
33
+ def decode_command(args) -> int:
34
+ output = args.output
35
+ if output is None:
36
+ base, ext = os.path.splitext(args.input)
37
+ output = base + ".out"
38
+
39
+ fs_in, rx = wavfile.read(args.input)
40
+ rx = rx.astype(np.float32)
41
+
42
+ if fs_in != SAMPLE_RATE:
43
+ print(f"Expected sample rate {SAMPLE_RATE}, got {fs_in}")
44
+ return 1
45
+
46
+ if rx.ndim == 2:
47
+ # Anti-phase stereo: subtracting the channels doubles the signal and cancels common noise
48
+ rx = (rx[:, 0] - rx[:, 1]) / 2
49
+
50
+ frames_payload, total_expected, stats = decode_data(rx, threads_num=args.threads, progress=True)
51
+
52
+ if total_expected is None:
53
+ print("No frames decoded.")
54
+ return 1
55
+
56
+ if args.fjf:
57
+ save_fjf(total_expected, frames_payload, args.fjf)
58
+ print(f"Saved {len(frames_payload)} frames into {args.fjf}")
59
+
60
+ data = reconstruct_data(frames_payload, total_expected)
61
+
62
+ open(output, 'wb').write(data)
63
+
64
+ print("Total frames expected:", total_expected)
65
+ print("Frames received:", len(frames_payload))
66
+ print(f"Decoded {len(data)} bytes into {output}")
67
+ print("Output CRC32:", zlib.crc32(data))
68
+
69
+ if len(frames_payload) < total_expected:
70
+ return 1
71
+ return 0
72
+
73
+
74
+ def main() -> int:
75
+ parser = argparse.ArgumentParser(prog="filejack", description="File encoding/decoding to audio signals.")
76
+ subparsers = parser.add_subparsers(dest="command", required=True)
77
+
78
+ encode_parser = subparsers.add_parser("encode", help="Encode a file into a WAV audio signal.")
79
+ encode_parser.add_argument("input", help="Path of the file to encode.")
80
+ encode_parser.add_argument("output", nargs='?', default=None, help="Path of the output WAV file. Defaults to <input>.wav")
81
+ encode_parser.add_argument("--mono", action="store_true", help="Write a mono WAV instead of anti-phase stereo.")
82
+ encode_parser.set_defaults(func=encode_command)
83
+
84
+ decode_parser = subparsers.add_parser("decode", help="Decode a WAV audio signal back into a file.")
85
+ decode_parser.add_argument("input", help="Path of the WAV file to decode.")
86
+ decode_parser.add_argument("output", nargs='?', default=None, help="Path of the output file. Defaults to <input without extension>.out")
87
+ decode_parser.add_argument("--fjf", default=None, help="Also save decoded frames into a .fjf file at this path.")
88
+ decode_parser.add_argument("--threads", type=int, default=12, help="Number of decoding threads. Defaults to 12.")
89
+ decode_parser.set_defaults(func=decode_command)
90
+
91
+ args = parser.parse_args()
92
+ return args.func(args)
93
+
94
+
95
+ if __name__ == '__main__':
96
+ raise SystemExit(main())
@@ -0,0 +1,23 @@
1
+ import numpy as np
2
+
3
+ from .values import *
4
+
5
+
6
+ def bytes_to_symbols(buf: bytes) -> np.ndarray:
7
+ b = np.frombuffer(buf, dtype=np.uint8)
8
+ bits = np.unpackbits(b)
9
+ assert bits.size % BITS_PER_SYMBOL == 0
10
+ bit3 = bits.reshape(-1, BITS_PER_SYMBOL)
11
+ symbols = np.zeros((bit3.shape[0],), dtype=np.int32)
12
+ for i in range(BITS_PER_SYMBOL):
13
+ symbols += bit3[:, i] << (BITS_PER_SYMBOL - 1 - i)
14
+ return symbols.astype(np.int32)
15
+
16
+
17
+ def symbols_to_bytes(symbols: np.ndarray) -> bytes:
18
+ symbols = symbols.astype(np.uint8) % DAPSK
19
+ bits = np.empty(symbols.size * BITS_PER_SYMBOL, dtype=np.uint8)
20
+ for i in range(BITS_PER_SYMBOL):
21
+ bits[i::BITS_PER_SYMBOL] = (symbols >> (BITS_PER_SYMBOL - 1 - i)) & 1
22
+ packed = np.packbits(bits)
23
+ return packed.tobytes()
@@ -0,0 +1,70 @@
1
+ import threading
2
+
3
+ import numpy as np
4
+
5
+ from .decode_frames import decode_frames
6
+ from .values import *
7
+
8
+
9
+ def decode_data(rx: np.ndarray, threads_num: int = 12, quick: bool = True, progress: bool = False):
10
+ """
11
+ Decode all frames from a mono sample buffer. Splits the buffer into overlapping
12
+ blocks and decodes them in parallel threads.
13
+ Returns (frames_payload, total_expected, stats) where stats is a dict with
14
+ no_lock, rsc_error, header_error and crc_error counters.
15
+ """
16
+ rx = np.asarray(rx, dtype=np.float32)
17
+
18
+ frames_payload = {}
19
+ stats = {'no_lock': 0, 'rsc_error': 0, 'header_error': 0, 'crc_error': 0}
20
+ total_expected = [None]
21
+ samples_done = [0]
22
+ limit = threading.BoundedSemaphore(threads_num)
23
+ lock = threading.Lock()
24
+ fs_block_size = SAMPLE_RATE * 5
25
+
26
+ def decode_block(block: np.ndarray):
27
+ try:
28
+ frames_block, total_expected1, ok_log1, no_lock1, rsc_error1, header_error1, crc_error1 = decode_frames(block, SAMPLE_RATE, search_step=SAMPLES_PER_SYMBOL, quick=quick)
29
+ with lock:
30
+ frames_payload.update(frames_block)
31
+
32
+ total_expected[0] = total_expected[0] or total_expected1
33
+
34
+ samples_done[0] += len(block) - int(SAMPLE_RATE * 0.2)
35
+
36
+ stats['no_lock'] += no_lock1
37
+ stats['rsc_error'] += rsc_error1
38
+ stats['header_error'] += header_error1
39
+ stats['crc_error'] += crc_error1
40
+
41
+ if progress:
42
+ print(f"\rBlock(Sample): {samples_done[0] // fs_block_size}/{len(rx) // fs_block_size}({samples_done[0]}/{len(rx)}) Frames: {len(frames_payload)}/{total_expected[0]} no_lock: {stats['no_lock']} rsc_error: {stats['rsc_error']} header_error: {stats['header_error']} crc_error: {stats['crc_error']}", end='')
43
+ except Exception as e:
44
+ print(f"\nError in thread: {e}")
45
+ finally:
46
+ limit.release()
47
+
48
+ threads: list[threading.Thread] = []
49
+
50
+ for i in range(0, len(rx), fs_block_size):
51
+ start = max(0, int(i - SAMPLE_RATE * 0.1))
52
+ end = min(len(rx), int(i + fs_block_size + SAMPLE_RATE * 0.1))
53
+
54
+ block = rx[start : end]
55
+
56
+ thread = threading.Thread(target=decode_block, args=(block,))
57
+ thread.start()
58
+ threads.append(thread)
59
+ limit.acquire()
60
+
61
+ threads = [t for t in threads if t.is_alive()]
62
+
63
+ while len(threads) > 0:
64
+ threads[0].join()
65
+ threads.pop(0)
66
+
67
+ if progress:
68
+ print()
69
+
70
+ return frames_payload, total_expected[0], stats
@@ -0,0 +1,244 @@
1
+ import zlib
2
+ import numpy as np
3
+ from reedsolo import ReedSolomonError
4
+ from scipy.signal import hilbert
5
+
6
+ from .RSC import *
7
+ from .conversions import *
8
+ from .values import *
9
+
10
+
11
+ def decode_frames(rx, sample_rate, search_step = 1, quick = False):
12
+ samples_per_symbol = int(round(sample_rate / baud_rate))
13
+ # sos_bp = butter(6, [carrier_freq - baud_rate * 2, carrier_freq + baud_rate * 2], btype='bandpass', fs=sample_rate, output='sos')
14
+ # rx = sosfiltfilt(sos_bp, rx).astype(np.float32)
15
+
16
+ pad_len = 2048
17
+ rx_padded = np.pad(rx, (pad_len, pad_len), 'constant')
18
+
19
+ analytic_padded = hilbert(rx_padded)
20
+
21
+ analytic = analytic_padded[pad_len:-pad_len]
22
+
23
+ num_symbols = int(len(rx) // samples_per_symbol)
24
+ n = np.arange(len(rx), dtype=np.int64)
25
+ t = n / sample_rate
26
+ bb = analytic * np.exp(-1j * 2*np.pi * carrier_freq * t)
27
+
28
+ # cut = 3500.0
29
+ # sos_lp = butter(6, cut, btype='lowpass', fs=sample_rate, output='sos')
30
+ # bb = sosfiltfilt(sos_lp, bb.real).astype(np.float32) + 1j * sosfiltfilt(sos_lp, bb.imag).astype(np.float32)
31
+
32
+ def symbols_from(bb, start_sample, offset, symbols_num):
33
+ start = start_sample + offset
34
+ need = start + symbols_num * samples_per_symbol
35
+ if need > len(bb):
36
+ return None
37
+
38
+ symbol = bb[start : start + symbols_num * samples_per_symbol].reshape(symbols_num, samples_per_symbol).sum(axis=1)
39
+
40
+ # phase = np.unwrap(np.angle(symbol))
41
+ # phase = np.angle(symbol)
42
+ # diff = np.diff(phase)
43
+ # diff = np.mod(diff, 2 * np.pi)
44
+ # diff = np.round(diff * PSK / 2 / np.pi).astype(np.int32) % PSK
45
+
46
+ return symbol
47
+
48
+ def steps_from(bb, start_sample, offset, n_symbols):
49
+ symbol = symbols_from(bb, start_sample, offset, n_symbols)
50
+
51
+ phase = np.angle(symbol[1:] * np.conj(symbol[:-1]))
52
+ diff = np.rint(np.mod(phase, 2*np.pi) * PSK/(2*np.pi)).astype(np.int32) % PSK
53
+
54
+ return diff
55
+
56
+ steps_by_offset = [steps_from(bb, 0, offset, (len(bb) - offset) // samples_per_symbol) for offset in range(samples_per_symbol)]
57
+
58
+ pattern = np.array(PREAMBLE_STEPS[1:] + SYNCWORD_STEPS, dtype=np.int32)
59
+ pattern_len = len(pattern)
60
+
61
+ PAYLOAD_STEPS_LEN = (N * 8) // BITS_PER_SYMBOL
62
+
63
+ frames_payload = {}
64
+ total_expected = None
65
+ min_match = int(0.9 * pattern_len)
66
+
67
+ no_lock = 0
68
+ rsc_error = 0
69
+ header_error = 0
70
+ crc_error = 0
71
+ ok_log = []
72
+
73
+ i = 0
74
+ while i + (pattern_len + PAYLOAD_STEPS_LEN + 1) * samples_per_symbol <= len(bb):
75
+ cands = []
76
+ for offset in range(samples_per_symbol):
77
+ # if i < offset:
78
+ # continue
79
+
80
+ start = max((i - offset) // samples_per_symbol, 0)
81
+ end = start + (pattern_len + PAYLOAD_STEPS_LEN + 1)
82
+ if end > len(steps_by_offset[offset]):
83
+ continue
84
+
85
+ steps = steps_by_offset[offset][start : end]
86
+ # steps = steps_from(bb, i, offset, pattern_len + PAYLOAD_STEPS_LEN + 1)
87
+ # if steps is None:
88
+ # continue
89
+
90
+ window = steps[:pattern_len]
91
+
92
+ delta = (window - pattern) % PSK
93
+ r = np.bincount(delta, minlength=PSK).argmax()
94
+ score = np.sum(delta == r)
95
+
96
+ if score >= min_match:
97
+ cands.append((score, offset, steps, start, r))
98
+
99
+ if not cands:
100
+ no_lock += 1
101
+ i += search_step
102
+ continue
103
+
104
+ cands.sort(key=lambda x: x[0], reverse=True)
105
+ decoded_ok = False
106
+ for best_score, best_off, best_steps, best_start_idx, rotation in cands:
107
+ # Extract payload steps immediately after pattern
108
+ payload_steps = best_steps[pattern_len:pattern_len + PAYLOAD_STEPS_LEN]
109
+ payload_steps = (payload_steps - rotation) % PSK
110
+
111
+ symbols = symbols_from(bb, best_start_idx * samples_per_symbol, best_off, pattern_len + PAYLOAD_STEPS_LEN + 1)
112
+
113
+ gain = np.median(abs(symbols[2 : pattern_len + 1])) + 1e-12 # epsilon - avoid div by zero
114
+ a_hat = abs(symbols[pattern_len + 1: pattern_len + 1 + PAYLOAD_STEPS_LEN]) / gain
115
+ a_hat = np.clip(a_hat, 0.0, 1.0)
116
+ d = np.abs(a_hat[:, None] - ASK_LEVELS[None, :])
117
+ amplitudes = np.argmin(d, axis=1)
118
+
119
+ combined = (amplitudes.astype(np.uint8) << PSK_BITS_PER_SYMBOL) | payload_steps.astype(np.uint8)
120
+ cw = symbols_to_bytes(combined)
121
+
122
+ # RS decode
123
+ try:
124
+ decoded = RSC.decode(cw)[0]
125
+ except ReedSolomonError:
126
+ # print("\rReed-Solomon decoding failed, skipping unusable frame.")
127
+ # print(f"\rMalformed frame: {seq}/{total} {payload_len}/{MAX_PAYLOAD}")
128
+
129
+ rsc_error += 1
130
+ continue
131
+
132
+ try:
133
+ hdr = decoded[:HDR_LEN]
134
+ seq, total, payload_len = struct.unpack(HDR_FMT, hdr)
135
+ payload = decoded[HDR_LEN:HDR_LEN + payload_len]
136
+
137
+ if payload_len > MAX_PAYLOAD or (total_expected is not None and (total != total_expected or seq >= total_expected)):
138
+ header_error += 1
139
+ continue
140
+
141
+ except struct.error:
142
+ header_error += 1
143
+ continue
144
+
145
+ try:
146
+ crc_recv = struct.unpack('>I', decoded[HDR_LEN + payload_len:HDR_LEN + payload_len + CRC_LEN])[0]
147
+ except struct.error:
148
+ print(f"\rFrame CRC unpacking failed: {seq}/{total_expected}")
149
+ print(decoded[HDR_LEN + payload_len:HDR_LEN + payload_len + CRC_LEN])
150
+ crc_error += 1
151
+ continue
152
+
153
+ crc_calc = zlib.crc32(hdr + payload) & 0xFFFFFFFF
154
+ if crc_recv != crc_calc:
155
+ print(f"\rCRC mismatch for frame {seq}: received {crc_recv}, calculated {crc_calc}")
156
+ crc_error += 1
157
+ continue
158
+
159
+ decoded_ok = True
160
+ break
161
+
162
+ if not decoded_ok:
163
+ i += search_step
164
+ continue
165
+
166
+ else:
167
+ try:
168
+ frames_payload[seq] = payload
169
+ total_expected = total_expected or total
170
+
171
+ frame_steps = pattern_len + PAYLOAD_STEPS_LEN + 1
172
+ frame_samples = frame_steps * samples_per_symbol
173
+ frame_start_sample = best_off + best_start_idx * samples_per_symbol
174
+ extra = 72
175
+
176
+ if len(ok_log) >= 2:
177
+ last_seq, last_start = ok_log[-1][0], ok_log[-1][1]
178
+
179
+ if last_seq < seq: # Normal forward progression
180
+ # Calculate actual spacing from last decoded frame
181
+ delta_seq = seq - last_seq
182
+ delta_samples = frame_start_sample - last_start
183
+ actual_spacing = delta_samples / delta_seq # samples per frame
184
+
185
+ # Predict next frame using observed spacing
186
+ expected = frame_start_sample + int(round(actual_spacing))
187
+ else:
188
+ # Fallback to nominal
189
+ expected = frame_start_sample + frame_samples + extra
190
+ else:
191
+ expected = frame_start_sample + frame_samples + extra
192
+
193
+
194
+ W = 16 * samples_per_symbol # try 4..8 symbols worth, start with 48 samples
195
+
196
+ best_local_score = -1
197
+ best_local_start = None
198
+ best_local_off = None
199
+ best_local_idx = None
200
+ best_local_rot = None
201
+
202
+ for cand_start_sample in range(expected - W, expected + W + 1):
203
+ if cand_start_sample < 0:
204
+ continue
205
+
206
+ off = cand_start_sample % samples_per_symbol
207
+ start_idx2 = cand_start_sample // samples_per_symbol
208
+ end2 = start_idx2 + (pattern_len + PAYLOAD_STEPS_LEN + 1)
209
+
210
+ if end2 > len(steps_by_offset[off]):
211
+ continue
212
+
213
+ steps2 = steps_by_offset[off][start_idx2:end2]
214
+ window2 = steps2[:pattern_len]
215
+
216
+ delta2 = (window2 - pattern) % PSK
217
+ r2 = np.bincount(delta2, minlength=PSK).argmax()
218
+ score2 = np.sum(delta2 == r2)
219
+
220
+ if score2 > best_local_score:
221
+ best_local_score = score2
222
+ best_local_start = cand_start_sample
223
+ best_local_off = off
224
+ best_local_idx = start_idx2
225
+ best_local_rot = r2
226
+
227
+ # snap if we found a good preamble near expected
228
+ if best_local_score >= min_match:
229
+ i = best_local_start
230
+ else:
231
+ if quick:
232
+ i = expected
233
+ else:
234
+ i += search_step
235
+
236
+
237
+ # ok_log.append((seq, frame_start_sample, expected, best_off, best_start_idx, best_score, rotation))
238
+ ok_log.append((seq, frame_start_sample, best_score))
239
+
240
+ except ValueError as e:
241
+ print(f"\rFrame parsing failed: {e}")
242
+ i += search_step
243
+
244
+ return frames_payload, total_expected, ok_log, no_lock, rsc_error, header_error, crc_error
@@ -0,0 +1,81 @@
1
+ import struct
2
+ import zlib
3
+ import numpy as np
4
+
5
+ from .values import *
6
+ from .conversions import *
7
+ from .RSC import *
8
+
9
+
10
+ def build_frames(data: bytes) -> list[bytearray]:
11
+ chunks = [data[i:i + MAX_PAYLOAD] for i in range(0, len(data), MAX_PAYLOAD)]
12
+
13
+ frames = []
14
+ for seq, payload in enumerate(chunks):
15
+ hdr = struct.pack(HDR_FMT, seq, len(chunks), len(payload))
16
+ crc = zlib.crc32(hdr + payload) & 0xFFFFFFFF
17
+ crc_b = struct.pack('>I', crc)
18
+
19
+ frame = hdr + payload + crc_b
20
+ frame += b"\x00" * (K - len(frame))
21
+
22
+ codeword = RSC.encode(frame)
23
+ frames.append(codeword)
24
+ return frames
25
+
26
+
27
+ def encode_frames(frames: list) -> np.ndarray:
28
+ if not frames:
29
+ return np.zeros(0, dtype=np.int16)
30
+
31
+ cursor = 0
32
+ all_samples = []
33
+ for cw in frames:
34
+ payload_steps = bytes_to_symbols(cw)
35
+
36
+ steps = np.concatenate([PREAMBLE_STEPS, SYNCWORD_STEPS, payload_steps % PSK])
37
+ steps = np.array(steps, dtype=np.int32)
38
+
39
+ diff = np.cumsum(steps) % PSK
40
+ amp_idx = (payload_steps / PSK).astype(np.int32)
41
+
42
+ amplitudes = ASK_LEVELS[amp_idx]
43
+
44
+ amplitudes = np.concatenate([np.full(len(PREAMBLE_STEPS), 1.0), np.full(len(SYNCWORD_STEPS), 1.0), amplitudes], dtype=np.float32)
45
+
46
+ phase_signal = np.repeat(diff, SAMPLES_PER_SYMBOL)
47
+ amplitudes = np.repeat(amplitudes, SAMPLES_PER_SYMBOL)
48
+
49
+ total_samples = len(phase_signal)
50
+
51
+ # Optional tiny fade-in to reduce click at frame boundary
52
+ fade_len = 2 * SAMPLES_PER_SYMBOL
53
+
54
+ ramp = np.linspace(0.0, 1.0, fade_len, endpoint=False, dtype=np.float32)
55
+
56
+ t_in = (np.arange(fade_len, dtype=np.int64) + cursor) / float(SAMPLE_RATE)
57
+ fade_in = float(amplitude) * np.cos(2 * np.pi * (carrier_freq * t_in + phase_signal[0] / PSK)) * ramp
58
+ cursor += fade_len
59
+
60
+ t = np.arange(total_samples, dtype=np.int64) + cursor
61
+ t = t / float(SAMPLE_RATE)
62
+ waveform = float(amplitude) * np.cos(2 * np.pi * (carrier_freq * t + phase_signal / PSK)) * amplitudes
63
+ cursor += total_samples
64
+
65
+ t_out = (np.arange(fade_len, dtype=np.int64) + cursor) / float(SAMPLE_RATE)
66
+ fade_out = float(amplitude) * np.cos(2 * np.pi * (carrier_freq * t_out + phase_signal[-1] / PSK)) * ramp[::-1]
67
+ cursor += fade_len
68
+
69
+ guard = np.zeros(fade_len, dtype=np.float32)
70
+ cursor += fade_len
71
+
72
+ waveform = np.concatenate([fade_in, waveform, fade_out, guard])
73
+
74
+ all_samples.append(waveform)
75
+
76
+ waveform = np.concatenate(all_samples)
77
+ return np.clip(np.rint(waveform), -32768, 32767).astype(np.int16)
78
+
79
+
80
+ def encode_data(data: bytes) -> np.ndarray:
81
+ return encode_frames(build_frames(data))
File without changes
@@ -0,0 +1,31 @@
1
+ import zlib
2
+ from scipy.io import wavfile
3
+ import numpy as np
4
+
5
+ from filejack.decode_data import decode_data
6
+ from filejack.reconstruct_data import reconstruct_data
7
+ from filejack.merge_frames import save_fjf
8
+ from filejack.values import *
9
+
10
+
11
+ file = "7z6"
12
+
13
+ fs_in, rx = wavfile.read(f"{file}.wav")
14
+ rx = rx.astype(np.float32)
15
+
16
+ assert fs_in == SAMPLE_RATE, f"Expected sample rate {SAMPLE_RATE}, got {fs_in}"
17
+
18
+ if rx.ndim == 2:
19
+ rx = (rx[:, 0] - rx[:, 1]) / 2
20
+
21
+ frames_payload, total_expected, stats = decode_data(rx, threads_num=12, progress=True)
22
+
23
+ save_fjf(total_expected, frames_payload, f"{file}.fjf")
24
+
25
+ data = reconstruct_data(frames_payload, total_expected)
26
+
27
+ open(f"out.{file}", 'wb').write(data)
28
+
29
+ print("Total frames expected:", total_expected)
30
+ print("Frames received:", len(frames_payload))
31
+ print("Output CRC32: ", zlib.crc32(data))
@@ -0,0 +1,18 @@
1
+ import zlib
2
+ import numpy as np
3
+ from scipy.io import wavfile
4
+
5
+ from filejack.encode_frames import encode_data
6
+ from filejack.values import *
7
+
8
+
9
+ file = "bmp"
10
+
11
+ data = open(f"in.{file}", 'rb').read()
12
+
13
+ waveform = encode_data(data)
14
+
15
+ stereo = np.column_stack([waveform, -waveform])
16
+ wavfile.write(f"{file}.wav", SAMPLE_RATE, stereo)
17
+
18
+ print(zlib.crc32(data))
@@ -0,0 +1,24 @@
1
+ from filejack.merge_frames import merge_frames, load_fjf, save_fjf
2
+ from filejack.reconstruct_data import reconstruct_data
3
+
4
+
5
+ def main():
6
+ path1 = "merged.fjf"
7
+ path2 = "decode3.fjf"
8
+ output_path = "merged.fjf"
9
+
10
+ total_expected, frames1 = load_fjf(path1)
11
+ total_expected, frames2 = load_fjf(path2)
12
+
13
+ merged_frames = merge_frames(total_expected, [frames1, frames2])
14
+ # merged_frames = merge_frames([frames2, frames1]) # Try reversing order to prefer frames from second file
15
+
16
+ save_fjf(total_expected, merged_frames, output_path)
17
+ print(f"Merged {len(frames1)} frames from {path1} and {len(frames2)} frames from {path2} into {len(merged_frames)} frames in {output_path}")
18
+
19
+ data = reconstruct_data(merged_frames, total_expected)
20
+ open("reconstructed.7z", "wb").write(data)
21
+
22
+
23
+ if __name__ == '__main__':
24
+ main()
@@ -0,0 +1,22 @@
1
+ """
2
+ Convert a stereo WAV file to mono by subtracting the right channel from the left channel. Should be used with encoder/decoder to cancel out the noise in stereo-jack transfers.
3
+ """
4
+
5
+ import numpy as np
6
+ from scipy.io import wavfile
7
+
8
+
9
+ def main():
10
+ sample_rate, rx = wavfile.read(f"stereo.wav")
11
+
12
+ stereo = np.array(rx).astype(np.float32)
13
+ mono = stereo[:, 0] - stereo[:, 1]
14
+ mono = mono / 2
15
+
16
+ mono = np.clip(np.rint(mono), -32768, 32767).astype(np.int16)
17
+
18
+ wavfile.write(f"mono.wav", sample_rate, mono)
19
+
20
+
21
+ if __name__ == '__main__':
22
+ main()
@@ -0,0 +1,59 @@
1
+ """
2
+ .fjf files is FileJack Frames file, containing multiple decoded frames with metadata. Can be used to merge results from different decoding attempts.
3
+ """
4
+
5
+ import struct
6
+
7
+
8
+ FILEJACK_FJF_HEADER = b'FJF0917\nhttps://github.com/Staheos/filejack\n1.0.0'
9
+
10
+
11
+ def merge_frames(total_expected: int, frames_inputs: list[dict[int, bytes | bytearray | list[int]]]):
12
+ frames_output = {}
13
+
14
+ for frames_input in frames_inputs:
15
+ for seq, payload in frames_input.items():
16
+ if seq not in frames_output:
17
+ frames_output[seq] = payload
18
+ return frames_output
19
+
20
+
21
+ def save_fjf(total_expected: int, frames: dict[int, bytes | bytearray | list[int]], filename: str):
22
+ with open(filename, 'wb') as f:
23
+
24
+ f.write(FILEJACK_FJF_HEADER)
25
+ f.write(struct.pack('>I', total_expected))
26
+ f.write(struct.pack('>I', len(frames)))
27
+
28
+ for seq in sorted(frames.keys()):
29
+ payload = frames[seq]
30
+ if isinstance(payload, list):
31
+ payload = bytes(payload)
32
+ payload_len = len(payload)
33
+ f.write(struct.pack('>I', seq)) # Frame sequence number
34
+ f.write(struct.pack('>I', payload_len)) # Payload length
35
+ f.write(payload) # Payload data
36
+
37
+
38
+ def load_fjf(filename: str) -> tuple[int, dict[int, bytes]]:
39
+ frames = {}
40
+ with open(filename, 'rb') as f:
41
+ # Read header
42
+ header = f.read(len(FILEJACK_FJF_HEADER))
43
+ if header != FILEJACK_FJF_HEADER:
44
+ raise ValueError("Invalid FJF file format")
45
+
46
+ total_expected_bytes = f.read(4)
47
+ total_expected = struct.unpack('>I', total_expected_bytes)[0]
48
+
49
+ num_frames_bytes = f.read(4)
50
+ num_frames = struct.unpack('>I', num_frames_bytes)[0]
51
+
52
+ for _ in range(num_frames):
53
+ seq_bytes = f.read(4)
54
+ payload_len_bytes = f.read(4)
55
+ seq = struct.unpack('>I', seq_bytes)[0]
56
+ payload_len = struct.unpack('>I', payload_len_bytes)[0]
57
+ payload = f.read(payload_len)
58
+ frames[seq] = payload
59
+ return total_expected, frames
@@ -0,0 +1,12 @@
1
+ from .RSC import MAX_PAYLOAD
2
+
3
+
4
+ def reconstruct_data(frames_payload, frames_num) -> bytes:
5
+ data = bytearray()
6
+ for i in range(0, frames_num):
7
+ if i not in frames_payload:
8
+ print(f"Missing frame {i}")
9
+ data.extend(b'\x00' * MAX_PAYLOAD)
10
+ continue
11
+ data.extend(frames_payload[i])
12
+ return bytes(data)
filejack/values.py ADDED
@@ -0,0 +1,32 @@
1
+ import math
2
+ import numpy as np
3
+
4
+ PSK = 8
5
+ PSK_BITS_PER_SYMBOL = int(math.log2(PSK))
6
+ assert 2 ** PSK_BITS_PER_SYMBOL == PSK
7
+
8
+
9
+ ASK_RINGS = 1
10
+ ASK_BITS_PER_SYMBOL = int(math.log2(ASK_RINGS))
11
+ assert 2 ** ASK_BITS_PER_SYMBOL == ASK_RINGS
12
+
13
+ if ASK_RINGS == 1:
14
+ ASK_LEVELS = np.array([1])
15
+ else:
16
+ ASK_LEVELS = np.linspace(0.4, 1.0, ASK_RINGS, dtype=np.float32)
17
+
18
+
19
+ DAPSK = PSK * ASK_RINGS
20
+ BITS_PER_SYMBOL = PSK_BITS_PER_SYMBOL + ASK_BITS_PER_SYMBOL
21
+
22
+
23
+ SAMPLE_RATE = 48000
24
+ baud_rate = 4000
25
+ amplitude = int(32767 * 0.8)
26
+ carrier_freq = 12000
27
+
28
+ SAMPLES_PER_SYMBOL = int(round(SAMPLE_RATE / baud_rate))
29
+
30
+
31
+ PREAMBLE_SYMS = 64
32
+ SYNCWORD_SYMS = 32
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: filejack
3
+ Version: 1.0.0
4
+ Summary: File encoding/decoding to audio signals.
5
+ Author: Staheos
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Staheos/FileJack
8
+ Project-URL: Source, https://github.com/Staheos/FileJack
9
+ Project-URL: Issues, https://github.com/Staheos/FileJack/issues
10
+ Keywords: file,audio,jack,encoder,decoder
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Operating System :: OS Independent
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: numpy>=1.15.0
23
+ Requires-Dist: scipy>=1.0.0
24
+ Requires-Dist: reedsolo>=1.0.0
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest>=8.0; extra == "test"
27
+ Requires-Dist: pytest-cov; extra == "test"
28
+ Requires-Dist: pytest-xdist[psutil]>=3.5; extra == "test"
29
+ Dynamic: license-file
30
+
31
+ # FileJack
32
+
33
+ File encoding/decoding to audio signals.
34
+
35
+ FileJack turns an arbitrary file into a WAV audio signal and back. The data is
36
+ carried on a 12 kHz carrier using differential 8-PSK modulation, protected with
37
+ Reed-Solomon error correction and a per-frame CRC. It is meant for sending files
38
+ over an audio link (for example a 3.5 mm jack cable or an acoustic channel).
39
+
40
+ ## How it works
41
+
42
+ The encoder splits the input into frames. Each frame carries a header
43
+ (sequence number, total frame count, payload length), the payload and a CRC32,
44
+ then gets Reed-Solomon encoded into a 255-byte codeword (223 data bytes + 32
45
+ parity bytes, so up to 209 payload bytes per frame). Each codeword is mapped to
46
+ symbols and modulated onto the carrier, preceded by a preamble and a syncword so
47
+ the decoder can find frame boundaries.
48
+
49
+ The decoder demodulates the recording, locks onto the preamble/syncword,
50
+ recovers the symbols, runs Reed-Solomon correction and checks each frame's CRC.
51
+ Valid frames are collected by sequence number and reassembled into the original
52
+ file. Decoding runs across multiple threads over overlapping blocks, so partial
53
+ or noisy recordings still recover whatever frames are intact.
54
+
55
+ Signal parameters live in [filejack/values.py](filejack/values.py): 48 kHz
56
+ sample rate, 4000 baud, 12 kHz carrier, 8-PSK.
57
+
58
+ ## Installation
59
+
60
+ ```
61
+ pip install filejack
62
+ ```
63
+
64
+ Or from source:
65
+
66
+ ```
67
+ pip install .
68
+ ```
69
+
70
+ Dependencies: numpy, scipy, reedsolo.
71
+
72
+ ## Command line usage
73
+
74
+ Encoding writes a WAV file. By default it produces an anti-phase stereo signal
75
+ (the right channel is the inverted left channel), which lets the decoder cancel
76
+ common-mode noise by subtracting the channels.
77
+
78
+ ```
79
+ filejack encode input.bin
80
+ filejack encode input.bin output.wav
81
+ filejack encode input.bin output.wav --mono
82
+ ```
83
+
84
+ Decoding reads a WAV file and writes the reconstructed file. Stereo input is
85
+ folded down automatically.
86
+
87
+ ```
88
+ filejack decode output.wav
89
+ filejack decode output.wav recovered.bin
90
+ ```
91
+
92
+ Decode options:
93
+
94
+ - `--fjf PATH` also saves the decoded frames into a `.fjf` file. This lets you
95
+ merge frames recovered from several recordings of the same transmission.
96
+ - `--threads N` sets the number of decoding threads (default 12).
97
+
98
+ The command is also available as `python -m filejack`.
99
+
100
+ ## Library usage
101
+
102
+ ```python
103
+ import zlib
104
+ from scipy.io import wavfile
105
+ import numpy as np
106
+
107
+ from filejack.encode_frames import encode_data
108
+ from filejack.decode_data import decode_data
109
+ from filejack.reconstruct_data import reconstruct_data
110
+ from filejack.values import SAMPLE_RATE
111
+
112
+ data = open("input.bin", "rb").read()
113
+
114
+ # Encode to samples and write a WAV
115
+ waveform = encode_data(data)
116
+ wavfile.write("output.wav", SAMPLE_RATE, waveform)
117
+
118
+ # Read a WAV and decode
119
+ fs, rx = wavfile.read("output.wav")
120
+ rx = rx.astype(np.float32)
121
+ if rx.ndim == 2:
122
+ rx = (rx[:, 0] - rx[:, 1]) / 2
123
+
124
+ frames_payload, total_expected, stats = decode_data(rx)
125
+ recovered = reconstruct_data(frames_payload, total_expected)
126
+
127
+ assert zlib.crc32(recovered) == zlib.crc32(data)
128
+ ```
129
+
130
+ More runnable scripts are in [filejack/examples](filejack/examples), including
131
+ `stereo_to_mono.py` for folding an anti-phase stereo recording down to mono.
132
+
133
+ ## .fjf files
134
+
135
+ A `.fjf` (FileJack Frames) file stores decoded frames with their sequence
136
+ numbers. Because each frame is independent, you can decode the same
137
+ transmission several times, save each attempt to a `.fjf`, and merge them to
138
+ fill in frames that were missed in individual passes. See
139
+ [filejack/merge_frames.py](filejack/merge_frames.py) and the merge example.
140
+
141
+ ## Development
142
+
143
+ ```
144
+ pip install .[test]
145
+ pytest
146
+ ```
147
+
148
+ ## License
149
+
150
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,23 @@
1
+ filejack/AsyncDecodeManager.py,sha256=g7-omESfySca1zNjFillfJ4AChBatExqd8UiLXwEPMk,6667
2
+ filejack/RSC.py,sha256=qD7XCWE82uJHTl6mjcKks-NUToxZZG88zjHydc6MOx8,1023
3
+ filejack/__init__.py,sha256=ZVeR32bA--Dy2akb_i2kGeskFSLR3n9TqKs4SK1bp3Y,312
4
+ filejack/__main__.py,sha256=D31U8_ux95qF64EQ8ReT25nabzxh7ped5avpobXz_bM,49
5
+ filejack/cli.py,sha256=y3EKLSLQpanJ0lQQuLlO9zXZHBLWNSbXacNpuhGbvSU,3142
6
+ filejack/conversions.py,sha256=rp0t9ZHTUJsMG9hvkYDHMcESaGfib1v1oaOjggRfckg,736
7
+ filejack/decode_data.py,sha256=zsBqw80kpDUhkb3dvLfvQ5TbNIKHXAYeKzMxMKMc9RM,2263
8
+ filejack/decode_frames.py,sha256=QvTw7nkc7kHVPFn8793-kcCiA4XrM2DeC3FPn6fyaQM,7587
9
+ filejack/encode_frames.py,sha256=SW-uM7yjtAZMbsN5QReDXzGuGAdCT5z8SjHifBZTDXo,2468
10
+ filejack/merge_frames.py,sha256=lfb86_hYjmFO0QjtQ_qbrGl7EqmgSezGrgr_yioAzuw,1827
11
+ filejack/reconstruct_data.py,sha256=AlD887NndfYXHui2Hp4D69X_2GzNUHIOM57a2N100jc,305
12
+ filejack/values.py,sha256=MOe7cRg8jNACeOe-e5tIzK2kmhIz_kBkdocXR4RiIgg,622
13
+ filejack/examples/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ filejack/examples/decoder.py,sha256=25bYNzR53FWyjA7ls81V6FQa7kMe_c2NOgrWmvhs4Dk,827
15
+ filejack/examples/encoder.py,sha256=k5tO3xe72lC703oUKT7-RY92EvKIj1m7hP7lZqJ89jg,347
16
+ filejack/examples/merge.py,sha256=7lNOBo5WNgDRaSNrU1blA5dWgzDZZ0IZcfQbSZh4N_0,807
17
+ filejack/examples/stereo_to_mono.py,sha256=PBgPaiDwSTPyMjAJ18b7XmkFyEAgmdXe05SV5kQS7B0,539
18
+ filejack-1.0.0.dist-info/licenses/LICENSE,sha256=C-SikHBZl70gMFwjvC9EqIKe4ki3pK0QM3L4Avm1MD8,1064
19
+ filejack-1.0.0.dist-info/METADATA,sha256=It0PBjUtDg-hxaPMXi1htnEpgmGnSbDfPfLJJ2vGOJg,4750
20
+ filejack-1.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
21
+ filejack-1.0.0.dist-info/entry_points.txt,sha256=sotGg7_o1BuSJOl4llcQ2ypWQ9bhFntHMHfZlyF4ocQ,47
22
+ filejack-1.0.0.dist-info/top_level.txt,sha256=67ReEJaubGZkfcf0WlBmxj3zoLXwoppOd-7AzbgqtlM,9
23
+ filejack-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ filejack = filejack.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Staheos
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 @@
1
+ filejack