Skip to content

Commit 49cfd77

Browse files
committed
Stream files instead of loading into memory
1 parent aff24ff commit 49cfd77

File tree

5 files changed

+44
-26
lines changed

5 files changed

+44
-26
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "binary-waterfall"
7-
version = "3.4.1"
7+
version = "3.5.0"
88
readme = "README_pypi.md"
99
description = "A Raw Data Media Player"
1010
license= {file = "LICENSE"}

src/binary_waterfall/core.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from . import window, constants
88

99
# TODO: Allow specifying custom audio and video bitrates (see OBS settings for ideas)
10-
# TODO: Add way to "stream" a binary file for the video frame generation instead of loading the file into memory
1110
# TODO: Add unit testing (https://realpython.com/python-testing/)
1211
# TODO: Add documentation (https://realpython.com/python-doctest/)
1312

src/binary_waterfall/generators.py

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def __init__(self,
4444
self.width = None
4545
self.dim = None
4646
self.total_bytes = None
47-
self.bytes = None
47+
self.file = None
4848
self.audio_filename = None
4949
self.flip_v = None
5050
self.flip_h = None
@@ -81,14 +81,19 @@ def __init__(self,
8181
def __del__(self):
8282
self.cleanup()
8383

84+
def close_file(self):
85+
if self.file is not None:
86+
self.file.close()
87+
self.file = None
88+
self.filename = None
89+
8490
def set_filename(self, filename):
8591
# Delete current audio file if it exists
8692
self.delete_audio()
8793

8894
if filename is None:
89-
# Reset all vars
90-
self.filename = None
91-
self.bytes = None
95+
# Reset all vars and close the file pointer
96+
self.close_file()
9297
self.total_bytes = None
9398
self.audio_filename = None
9499
return
@@ -98,10 +103,11 @@ def set_filename(self, filename):
98103

99104
self.filename = os.path.realpath(filename)
100105

101-
# Load bytes
102-
with open(self.filename, "rb") as f:
103-
self.bytes = f.read()
104-
self.total_bytes = len(self.bytes)
106+
# Open file
107+
self.file = open(self.filename, "rb")
108+
109+
# Get total number of bytes
110+
self.total_bytes = os.stat(self.filename).st_size
105111

106112
# Compute audio file name
107113
file_path, file_main_name = os.path.split(self.filename)
@@ -325,7 +331,8 @@ def compute_audio(self):
325331
f.setnchannels(self.num_channels)
326332
f.setsampwidth(self.sample_bytes)
327333
f.setframerate(self.sample_rate)
328-
f.writeframesraw(self.bytes)
334+
for chunk in iter(lambda: self.file.read(4096), b""):
335+
f.writeframesraw(chunk)
329336

330337
if self.volume != 100:
331338
# Reduce the audio volume
@@ -344,6 +351,10 @@ def change_filename(self, new_filename):
344351
self.set_filename(new_filename)
345352
self.compute_audio()
346353

354+
def get_file_bytes(self, address, count):
355+
self.file.seek(address)
356+
return self.file.read(count)
357+
347358
def get_address(self, ms):
348359
# Get the size of a single "block" (a row, we only move in increments of 1 row)
349360
address_block_size = self.width * self.color_bytes
@@ -375,8 +386,15 @@ def get_frame_bytestring(self, ms):
375386
picture_bytes += b"\x00" * 3 * round(-address / self.color_bytes)
376387
address = 0
377388

389+
# Get the maximum number of bytes that could be used for this frame
390+
frame_bytes = self.get_file_bytes(
391+
address=address,
392+
count=(self.width * self.height * self.color_bytes)
393+
)
394+
378395
full_length = (self.width * self.height * 3)
379396

397+
idx = 0
380398
for row in range(self.height):
381399
for col in range(self.width):
382400
# If we already have a full frame, stop the loops
@@ -387,27 +405,27 @@ def get_frame_bytestring(self, ms):
387405
this_byte = [b'\x00', b'\x00', b'\x00']
388406
for c in self.color_format:
389407
if c == constants.ColorFmtCode.RED:
390-
this_byte[0] = self.bytes[address:address + 1] # Red
408+
this_byte[0] = frame_bytes[idx:idx + 1] # Red
391409
elif c == constants.ColorFmtCode.RED_INV:
392-
this_byte[0] = helpers.invert_bytes(self.bytes[address:address + 1]) # Red inverted
410+
this_byte[0] = helpers.invert_bytes(frame_bytes[idx:idx + 1]) # Red inverted
393411
elif c == constants.ColorFmtCode.GREEN:
394-
this_byte[1] = self.bytes[address:address + 1] # Green
412+
this_byte[1] = frame_bytes[idx:idx + 1] # Green
395413
elif c == constants.ColorFmtCode.GREEN_INV:
396-
this_byte[1] = helpers.invert_bytes(self.bytes[address:address + 1]) # Green inverted
414+
this_byte[1] = helpers.invert_bytes(frame_bytes[idx:idx + 1]) # Green inverted
397415
elif c == constants.ColorFmtCode.BLUE:
398-
this_byte[2] = self.bytes[address:address + 1] # Blue
416+
this_byte[2] = frame_bytes[idx:idx + 1] # Blue
399417
elif c == constants.ColorFmtCode.BLUE_INV:
400-
this_byte[2] = helpers.invert_bytes(self.bytes[address:address + 1]) # Blue inverted
418+
this_byte[2] = helpers.invert_bytes(frame_bytes[idx:idx + 1]) # Blue inverted
401419
elif c == constants.ColorFmtCode.WHITE:
402-
this_byte[0] = self.bytes[address:address + 1] # Red
403-
this_byte[1] = self.bytes[address:address + 1] # Green
404-
this_byte[2] = self.bytes[address:address + 1] # Blue
420+
this_byte[0] = frame_bytes[idx:idx + 1] # Red
421+
this_byte[1] = frame_bytes[idx:idx + 1] # Green
422+
this_byte[2] = frame_bytes[idx:idx + 1] # Blue
405423
elif c == constants.ColorFmtCode.WHITE_INV:
406-
this_byte[0] = helpers.invert_bytes(self.bytes[address:address + 1]) # Red inverted
407-
this_byte[1] = helpers.invert_bytes(self.bytes[address:address + 1]) # Green inverted
408-
this_byte[2] = helpers.invert_bytes(self.bytes[address:address + 1]) # Blue inverted
424+
this_byte[0] = helpers.invert_bytes(frame_bytes[idx:idx + 1]) # Red inverted
425+
this_byte[1] = helpers.invert_bytes(frame_bytes[idx:idx + 1]) # Green inverted
426+
this_byte[2] = helpers.invert_bytes(frame_bytes[idx:idx + 1]) # Blue inverted
409427

410-
address += 1
428+
idx += 1
411429

412430
picture_bytes += b"".join(this_byte)
413431
else:
@@ -451,6 +469,7 @@ def get_frame_qimage(self, ms):
451469
return qimg
452470

453471
def cleanup(self):
472+
self.close_file()
454473
self.delete_audio()
455474
shutil.rmtree(self.temp_dir)
456475

src/binary_waterfall/outputs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ def export_video(self,
414414
if progress_dialog.wasCanceled():
415415
shutil.rmtree(temp_dir)
416416
return
417-
progress_dialog.setLabelText("Splicing final video file (program may lag)...")
417+
progress_dialog.setLabelText("Splicing final video file... (program may lag)")
418418

419419
# Export audio
420420
self.export_audio(audio_file)

src/binary_waterfall/version.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Version: 3.4.1
1+
Version: 3.5.0
22
CompanyName: Ella Jameson (nimaid)
33
FileDescription: A Raw Data Media Player
44
InternalName: Binary Waterfall

0 commit comments

Comments
 (0)