text: # pcm.py
  # Converts between .wav files and 1-bit pcm data. (pcm = pulse-code modulation)
  
  import argparse
  import os
  import struct
  import wave
  
  
  BASE_SAMPLE_RATE = 22050
  
  def convert_to_wav(filenames=[]):
      """
      Converts a file containing 1-bit pcm data into a .wav file.
      """
      for filename in filenames:
          with open(filename, 'rb') as pcm_file:
              # Generate array of on/off pcm values.
              samples = []
              byte = pcm_file.read(1)
              while byte != "":
                  byte = struct.unpack('B', byte)[0]
                  for i in range(8):
                      bit_index = 7 - i
                      value = (byte >> bit_index) & 1
                      samples.append(value)
                  byte = pcm_file.read(1)
  
          # Write a .wav file using the pcm data.
          name, extension = os.path.splitext(filename)
          wav_filename = name + '.wav'
          wave_file = wave.open(wav_filename, 'w')
          wave_file.setframerate(BASE_SAMPLE_RATE)
          wave_file.setnchannels(1)
          wave_file.setsampwidth(1)
  
          for value in samples:
              if value > 0:
                  value = 0xff
  
              packed_value = struct.pack('B', value)
              wave_file.writeframesraw(packed_value)
  
          wave_file.close()
  
  
  def convert_to_pcm(filenames=[]):
      """
      Converts a .wav file into 1-bit pcm data.
      Samples in the .wav file are simply clamped to on/off.
  
      This currently works correctly on .wav files with the following attributes:
              1. Sample Width = 1 or 2 bytes (Some wave files use 3 bytes per sample...)
              2. Arbitrary sample sample_rate
              3. Mono or Stereo (1 or 2 channels)
      """
      for filename in filenames:
          samples, average_sample = get_wav_samples(filename)
  
          # Generate a list of clamped samples
          clamped_samples = []
          for sample in samples:
              # Clamp the raw sample to on/off
              if sample < average_sample:
                  clamped_samples.append(0)
              else:
                  clamped_samples.append(1)
  
          # The pcm data must be a multiple of 8, so pad the clamped samples with 0.
          while len(clamped_samples) % 8 != 0:
              clamped_samples.append(0)
  
          # Pack the 1-bit samples together.
          packed_samples = bytearray()
          for i in xrange(0, len(clamped_samples), 8):
              # Read 8 pcm values to pack one byte.
              packed_value = 0
              for j in range(8):
                  packed_value <<= 1
                  packed_value += clamped_samples[i + j]
              packed_samples.append(packed_value)
  
          # Open the output .pcm file, and write all 1-bit samples.
          name, extension = os.path.splitext(filename)
          pcm_filename = name + '.pcm'
          with open(pcm_filename, 'wb') as out_file:
              out_file.write(packed_samples)
  
  
  def get_wav_samples(filename):
      """
      Reads the given .wav file and returns a list of its samples after re-sampling
      to BASE_SAMPLE_RATE.
      Also returns the average sample amplitude.
      """
      wav_file = wave.open(filename, 'r')
      sample_width = wav_file.getsampwidth()
      sample_count = wav_file.getnframes()
      sample_rate = wav_file.getframerate()
      num_channels = wav_file.getnchannels()
  
      samples = bytearray(wav_file.readframes(sample_count))
  
      # Unpack the values based on the sample byte width.
      unpacked_samples = []
      for i in xrange(0, len(samples), sample_width):
          if sample_width == 1:
              fmt = 'B'
          elif sample_width == 2:
              fmt = 'h'
          else:
              # todo: support 3-byte sample width
              raise Exception, "Unsupported sample width: " + str(sample_width)
  
          value = struct.unpack(fmt, samples[i:i + sample_width])[0]
          unpacked_samples.append(value)
  
      # Only keep the samples from the first audio channel.
      unpacked_samples = unpacked_samples[::num_channels]
  
      # Approximate the BASE_SAMPLE_RATE.
      # Also find the average amplitude of the samples.
      resampled_samples = []
      total_value = 0
      interval = float(sample_rate) / BASE_SAMPLE_RATE
      index = 0
      while index < sample_count:
          sample = unpacked_samples[int(index)]
          total_value += sample
  
          resampled_samples.append(sample)
          index += interval
  
      average_sample = float(total_value) / len(resampled_samples)
  
      return resampled_samples, average_sample
  
  
  def main():
      ap = argparse.ArgumentParser()
      ap.add_argument('mode')
      ap.add_argument('filenames', nargs='*')
      args = ap.parse_args()
  
      method = {
          'wav': convert_to_wav,
          'pcm': convert_to_pcm,
      }.get(args.mode, None)
  
      if method == None:
          raise Exception, "Unknown conversion method!"
  
      method(args.filenames)
  
  if __name__ == "__main__":
      main()
  