pngtowav.py
1 # SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries 2 # 3 # SPDX-License-Identifier: MIT 4 5 #!/usr/bin/python3 6 7 ### pngtowav v1.0 8 """Convert a list of png images to pseudo composite video in wav file form. 9 10 This is Python code not intended for running on a microcontroller board. 11 """ 12 13 ### MIT License 14 15 ### Copyright (c) 2019 Kevin J. Walters 16 17 ### Permission is hereby granted, free of charge, to any person obtaining a copy 18 ### of this software and associated documentation files (the "Software"), to deal 19 ### in the Software without restriction, including without limitation the rights 20 ### to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 21 ### copies of the Software, and to permit persons to whom the Software is 22 ### furnished to do so, subject to the following conditions: 23 24 ### The above copyright notice and this permission notice shall be included in all 25 ### copies or substantial portions of the Software. 26 27 ### THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 ### IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 ### FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 ### AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 ### LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 32 ### OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 33 ### SOFTWARE. 34 35 import getopt 36 import sys 37 import array 38 import wave 39 40 import imageio 41 42 43 ### globals 44 ### pylint: disable=invalid-name 45 ### start_offset of 1 can help if triggering on oscilloscope 46 ### is missing alternate lines 47 debug = 0 48 verbose = False 49 movie_file = False 50 output_filename = "dacanim.wav" 51 fps = 50 52 threshold = 128 ### pixel level 53 replaceforsync = False 54 start_offset = 1 55 56 max_dac_v = 3.3 57 ### 16 bit wav files always use signed representation for data 58 dac_offtop = 2**15-1 ### 3.30V 59 dac_sync = -2**15 ### 0.00V 60 ### image from 3.00V to 0.30V 61 dac_top = round(3.00 / max_dac_v * (2**16-1)) - 2**15 62 dac_bottom = round(0.30 / max_dac_v * (2**16-1)) - 2**15 63 64 65 def usage(exit_code): ### pylint: disable=missing-docstring 66 print("pngtowav: " 67 + "[-d] [-f fps] [-h] [-m] [-o outputfilename] [-r] [-s lineoffset] [-t threshold] [-v]", 68 file=sys.stderr) 69 if exit_code is not None: 70 sys.exit(exit_code) 71 72 73 def image_to_dac(img, row_offset, first_pix, dac_y_range): 74 """Convert a single image to DAC output.""" 75 dac_out = array.array("h", []) 76 77 img_height, img_width = img.shape 78 if verbose: 79 print("W,H", img_width, img_height) 80 81 for row_o in range(img_height): 82 row = (row_o + row_offset) % img_height 83 ### Currently using 0 to (n-1)/n range 84 y_pos = round(dac_top - row / (img_height - 1) * dac_y_range) 85 if verbose: 86 print("Adding row", row, "at y_pos", y_pos) 87 dac_out.extend(array.array("h", 88 [dac_sync] 89 + [y_pos if x >= threshold else dac_offtop 90 for x in img[row, first_pix:]])) 91 return dac_out, img_width, img_height 92 93 94 def write_wav(filename, data, framerate): 95 """Create one channel 16bit wav file.""" 96 wav_file = wave.open(filename, "w") 97 nchannels = 1 98 sampwidth = 2 99 nframes = len(data) 100 comptype = "NONE" 101 compname = "not compressed" 102 if verbose: 103 print("Writing wav file", filename, "at rate", framerate, 104 "with", nframes, "samples") 105 wav_file.setparams((nchannels, sampwidth, framerate, nframes, 106 comptype, compname)) 107 wav_file.writeframes(data) 108 wav_file.close() 109 110 111 def main(cmdlineargs): ### pylint: disable=too-many-branches 112 """main(args)""" 113 global debug, fps, movie_file, output_filename, replaceforsync ### pylint: disable=global-statement 114 global threshold, start_offset, verbose ### pylint: disable=global-statement 115 116 try: 117 opts, args = getopt.getopt(cmdlineargs, 118 "f:hmo:rs:t:v", ["help", "output="]) 119 except getopt.GetoptError as err: 120 print(err, 121 file=sys.stderr) 122 usage(2) 123 for opt, arg in opts: 124 if opt == "-d": ### pylint counts these towards too-many-branches :( 125 debug = 1 126 elif opt == "-f": 127 fps = int(arg) 128 elif opt in ("-h", "--help"): 129 usage(0) 130 elif opt == "-m": 131 movie_file = True 132 elif opt in ("-o", "--output"): 133 output_filename = arg 134 elif opt == "-r": 135 replaceforsync = True 136 elif opt == "-s": 137 start_offset = int(arg) 138 elif opt == "-t": 139 threshold = int(arg) 140 elif opt == "-v": 141 verbose = True 142 else: 143 print("Internal error: unhandled option", 144 file=sys.stderr) 145 sys.exit(3) 146 147 dac_samples = array.array("h", []) 148 149 ### Decide whether to replace first column with sync pulse 150 ### or add it as an additional column 151 first_pix = 1 if replaceforsync else 0 152 153 ### Read each frame, either 154 ### many single image filenames in args or 155 ### one or more video (animated gifs) (needs -m on command line) 156 dac_y_range = dac_top - dac_bottom 157 row_offset = 0 158 for arg in args: 159 if verbose: 160 print("PROCESSING", arg) 161 if movie_file: 162 images = imageio.mimread(arg) 163 else: 164 images = [imageio.imread(arg)] 165 166 for img in images: 167 img_output, width, height = image_to_dac(img, row_offset, 168 first_pix, dac_y_range) 169 dac_samples.extend(img_output) 170 row_offset += start_offset 171 172 write_wav(output_filename, dac_samples, 173 (width + (1 - first_pix)) * height * fps) 174 175 176 if __name__ == "__main__": 177 main(sys.argv[1:])