/ CPX_DAC_Guide / python / pngtowav.py
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:])