summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/pokemontools/pcm.py156
1 files changed, 156 insertions, 0 deletions
diff --git a/tools/pokemontools/pcm.py b/tools/pokemontools/pcm.py
new file mode 100644
index 00000000..428d5730
--- /dev/null
+++ b/tools/pokemontools/pcm.py
@@ -0,0 +1,156 @@
+# 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 range(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 range(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()