diff options
Diffstat (limited to 'tools')
-rw-r--r-- | tools/.gitignore | 1 | ||||
-rw-r--r-- | tools/Makefile | 2 | ||||
-rw-r--r-- | tools/pcm.c | 160 | ||||
-rw-r--r-- | tools/pokemontools/pcm.py | 156 | ||||
-rwxr-xr-x | tools/unnamed.py | 6 |
5 files changed, 321 insertions, 4 deletions
diff --git a/tools/.gitignore b/tools/.gitignore index 967af106..cf5f5adb 100644 --- a/tools/.gitignore +++ b/tools/.gitignore @@ -1,3 +1,4 @@ scan_includes gfx pkmncompress +pcm diff --git a/tools/Makefile b/tools/Makefile index 7ab1d146..6bea053d 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -3,7 +3,7 @@ CC := gcc CFLAGS := -O3 -std=c99 -Wall -Wextra -Wno-missing-field-initializers -tools := scan_includes gfx pkmncompress +tools := scan_includes gfx pkmncompress pcm all: $(tools) @: diff --git a/tools/pcm.c b/tools/pcm.c new file mode 100644 index 00000000..a14e291d --- /dev/null +++ b/tools/pcm.c @@ -0,0 +1,160 @@ +#include <stdio.h> +#include <stdlib.h> +#include <stdint.h> +#include <string.h> + +#define CHUNKID(b1, b2, b3, b4) \ + (uint32_t)((uint32_t)(b1) | ((uint32_t)(b2) << 8) | \ + ((uint32_t)(b3) << 16) | ((uint32_t)(b4) << 24)) + +size_t file_size(FILE *f) { + if (fseek(f, 0, SEEK_END) == -1) return 0; + long f_size = ftell(f); + if (f_size == -1) return 0; + if (fseek(f, 0, SEEK_SET) == -1) return 0; + return (size_t)f_size; +} + +int32_t get_uint16le(uint8_t *data, size_t size, size_t i) { + return i + 2 >= size ? -1 : + (int32_t)data[i] | ((int32_t)data[i+1] << 8); +} + +int64_t get_uint32le(uint8_t *data, size_t size, size_t i) { + return i + 4 >= size ? -1 : + (int64_t)data[i] | ((int64_t)data[i+1] << 8) | + ((int64_t)data[i+2] << 16) | ((int64_t)data[i+3] << 24); +} + +uint8_t *wav2pcm(uint8_t *wavdata, size_t wavsize, size_t *pcmsize) { + int64_t fourcc = get_uint32le(wavdata, wavsize, 0); + if (fourcc != CHUNKID('R', 'I', 'F', 'F')) { + fputs("WAV file does not start with 'RIFF'\n", stderr); + return NULL; + } + + int64_t waveid = get_uint32le(wavdata, wavsize, 8); + if (waveid != CHUNKID('W', 'A', 'V', 'E')) { + fputs("RIFF chunk does not start with 'WAVE'\n", stderr); + return NULL; + } + + size_t sample_offset = 0; + int64_t num_samples = 0; + + size_t riffsize = (size_t)get_uint32le(wavdata, wavsize, 4) + 8; + for (size_t i = 12; i < riffsize;) { + int64_t chunkid = get_uint32le(wavdata, wavsize, i); + int64_t chunksize = get_uint32le(wavdata, wavsize, i+4); + i += 8; + if (chunksize == -1) { + fputs("failed to read sub-chunk size\n", stderr); + return NULL; + } + + // require 22050 Hz 8-bit PCM WAV audio + if (chunkid == CHUNKID('f', 'm', 't', ' ')) { + int32_t audio_format = get_uint16le(wavdata, wavsize, i); + if (audio_format != 1) { + fputs("WAV data is not PCM format\n", stderr); + return NULL; + } + int32_t num_channels = get_uint16le(wavdata, wavsize, i+2); + if (num_channels != 1) { + fputs("WAV data is not mono\n", stderr); + return NULL; + } + int64_t sample_rate = get_uint32le(wavdata, wavsize, i+4); + if (sample_rate != 22050) { + fputs("WAV data is not 22050 Hz\n", stderr); + return NULL; + } + int32_t bits_per_sample = get_uint16le(wavdata, wavsize, i+14); + if (bits_per_sample != 8) { + fputs("WAV data is not 8-bit\n", stderr); + return NULL; + } + } + + else if (chunkid == CHUNKID('d', 'a', 't', 'a')) { + sample_offset = i; + num_samples = chunksize; + break; + } + + i += (size_t)chunksize; + } + + if (!num_samples) { + fputs("WAV data has no PCM samples\n", stderr); + return NULL; + } + + // pack 8 WAV samples per PCM byte, clamping each to 0 or 1 + *pcmsize = (size_t)((num_samples + 7) / 8); + uint8_t *pcmdata = malloc(*pcmsize); + for (int64_t i = 0; i < num_samples; i += 8) { + uint8_t v = 0; + for (int64_t j = 0; j < 8 && i + j < num_samples; j++) { + v |= (wavdata[sample_offset + i + j] > 0x80) << (7 - j); + } + pcmdata[i / 8] = v; + } + + return pcmdata; +} + +int main(int argc, char *argv[]) { + if (argc != 3) { + fprintf(stderr, "Usage: %s infile.wav outfile.pcm\n", argv[0]); + return EXIT_FAILURE; + } + + char *wavname = argv[1]; + char *pcmname = argv[2]; + + FILE *wavfile = fopen(wavname, "rb"); + if (!wavfile) { + fprintf(stderr, "failed to open for reading: '%s'\n", wavname); + return EXIT_FAILURE; + } + + size_t wavsize = file_size(wavfile); + if (!wavsize) { + fclose(wavfile); + fprintf(stderr, "failed to get file size: '%s'\n", wavname); + return EXIT_FAILURE; + } + + uint8_t *wavdata = malloc(wavsize); + size_t readsize = fread(wavdata, 1, wavsize, wavfile); + fclose(wavfile); + if (readsize != wavsize) { + fprintf(stderr, "failed to read: '%s'\n", wavname); + return EXIT_FAILURE; + } + + size_t pcmsize; + uint8_t *pcmdata = wav2pcm(wavdata, wavsize, &pcmsize); + free(wavdata); + if (!pcmdata) { + fprintf(stderr, "failed to convert: '%s'\n", wavname); + return EXIT_FAILURE; + } + + FILE *pcmfile = fopen(pcmname, "wb"); + if (!pcmfile) { + fprintf(stderr, "failed to open for writing: '%s'\n", pcmname); + return EXIT_FAILURE; + } + + size_t writesize = fwrite(pcmdata, 1, pcmsize, pcmfile); + free(pcmdata); + fclose(pcmfile); + if (writesize != pcmsize) { + fprintf(stderr, "failed to write: '%s'\n", pcmname); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} 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() diff --git a/tools/unnamed.py b/tools/unnamed.py index c5544437..d3a8b6bf 100755 --- a/tools/unnamed.py +++ b/tools/unnamed.py @@ -40,8 +40,8 @@ objects = None if args.rootdir: for line in Popen(["make", "-C", args.rootdir, "-s", "-p", "DEBUG=1"], stdout=PIPE).stdout.read().decode().split("\n"): - if line.startswith("pokered_obj := "): - objects = line[15:].strip().split() + if line.startswith("rom_obj := "): + objects = line[11:].strip().split() break else: print("Error: Object files not found!", file=stderr) @@ -63,7 +63,7 @@ for line in args.symfile: symbols.add(symbol) # If no object files were provided, just print what we know and exit -print("Unnamed pokered symbols: %d (%.2f%% complete)" % (len(symbols), +print("Unnamed pokeyellow symbols: %d (%.2f%% complete)" % (len(symbols), (symbols_total - len(symbols)) / symbols_total * 100)) if not objects: for sym in symbols: |