<reponame>Vsdparibas/musicboy
"""
Usage: flashprep.py path_to_audio_directory

This script will take the audio files in the passed directory,
sort them lexicographically, pick a sample rate to fit them
into the provided memory space, and then output:
  - flash.bin, containing a header plus the audio streams
  - data/flash_constants.h, containing constants
  - data/flash_constants.c, containing an TrackMetadata array
"""

import os
import sys
import math
import struct
from glob import glob
from tqdm import tqdm
import subprocess


SECRET_HEADER_TEXT = """
Hello world! If you're reading this, you've somehow
disassembled this custom Game Boy cartridge and are
reverse engineering it or something. Cool. My name's
Peter, and I built this in 2017/2018. I'd love to
hear from you - send me an email at:
<EMAIL> or find me on most social
networks at @psobot.

What follows is a concatenation of multiple audio
files encoded at 8bits and some arbitrary sample rate.
Their bounds are specified in a rudimentary header format:

uint32_t: number of files
uint32_t: start of file 0
uint32_t: start of file 1...

After the newline at the end of the next sentence,
the header will begin. Happy hacking!
"""

SECRET_HEADER_TEXT = SECRET_HEADER_TEXT.lstrip()
DEFAULT_SIZE_IN_MBITS = 128
MAX_NUMBER_OF_HEX_DIGITS = math.log(DEFAULT_SIZE_IN_MBITS, 2) - 1


def generate_file_offsets(files_with_sizes):
    offset = get_header_size(files_with_sizes)
    for file, size in files_with_sizes:
        yield (file, offset)
        offset += size


def generate_header(files_with_sizes):
    return "".join([
        SECRET_HEADER_TEXT,
        struct.pack("<i", len(files_with_sizes)),
    ] + [
        struct.pack("<i", offset)
        for _, offset
        in generate_file_offsets(files_with_sizes)
    ])


def get_header_table_size(files):
    return 4 + (len(files) * 4)


def get_header_table_start():
    return len(SECRET_HEADER_TEXT)


def get_header_size(files):
    return get_header_table_start() + get_header_table_size(files)


def to_raw_audio(file, sample_rate=None):
    if sample_rate:
        sample_rate_args = ['-ar', str(sample_rate)]
    else:
        sample_rate_args = []

    command = [
        'ffmpeg', '-i', file, '-f', 'u8', '-ac', '1'
    ] + sample_rate_args + ['-']

    return subprocess.check_output(
        command,
        stderr=open(os.devnull, 'w')
    )


def normalize_name(fname):
    return (
        fname
        .split('/')[-1]
        .split('.')[0]
        .upper()
        .replace(' ', '_')
        .replace("(", "")
        .replace(")", ""))


def generate_c_defines(offsets, files_with_sizes, sample_rate, buffer_size):
    yield "#ifndef flash_constants"
    yield "#define flash_constants"
    yield "#define BUFFER_SIZE_BYTE {}".format(buffer_size)
    yield "#define AUDIO_TRACK_COUNT {}".format(len(files_with_sizes))

    yield "#define AUDIO_SAMPLE_RATE {}".format(sample_rate)
    for (file, offset), (_, size) in zip(offsets, files_with_sizes):
        yield "#define AUDIO_{}_START {}".format(normalize_name(file), offset)
        yield "#define AUDIO_{}_END {}".format(
            normalize_name(file), offset + size)

    yield "typedef struct {"
    yield "  unsigned int start_offset;"
    yield "  unsigned int end_offset;"
    yield "} TrackMetadataEntry;"

    yield "TrackMetadataEntry TrackMetadata[AUDIO_TRACK_COUNT];"
    yield ("#define TrackMetadataForLevel(level) "
           "(TrackMetadata[level % AUDIO_TRACK_COUNT])")
    yield "#endif"


def generate_c_arrays(offsets, files_with_sizes):
    yield '#include "flash_constants.h"'

    """
    Note: The original Super Mario Land cart uses this music mapping:

    .musicByLevel
        db 7, 7, 3
        db 8, 8, 5
        db 7, 3, 3
        db 6, 6 ,5
    """

    yield "TrackMetadataEntry TrackMetadata[AUDIO_TRACK_COUNT] = {"
    for (file, offset), (_, size) in zip(offsets, files_with_sizes):
        yield "\t{{ AUDIO_{}_START, AUDIO_{}_END }},".format(
            normalize_name(file),
            normalize_name(file))
    yield "};"


def get_total_audio_length(files):
    # assume original files are at 44100
    sample_rate = 44100
    converted_audio_files = [to_raw_audio(f, sample_rate) for f in tqdm(files)]
    return float(sum(len(file) for file in converted_audio_files)) \
        / float(sample_rate)


def as_hex_address(num):
    return ("0x{:0" + str(int(MAX_NUMBER_OF_HEX_DIGITS)) + "X}").format(num)


def flashprep(
    directory,
    outfile='flash.bin',
    maxsize=DEFAULT_SIZE_IN_MBITS * 1024 * 1024 / 8,
    sample_rate=None,
    buffer_size=512,
):
    """
    Given a list of files, generate one binary with all of them concatenated,
    along with C defines specifying their bounds.
    """
    files = sorted(glob(os.path.join(directory, '*')))

    if sample_rate is None:
        quantize = 50
        available_space = maxsize - get_header_size(files)
        audio_length_seconds = get_total_audio_length(files)
        float_sample_rate = (float(available_space) / audio_length_seconds)
        sample_rate = math.floor(float_sample_rate / quantize) * quantize
        print "Using sample rate of {:,} to fit {:,}s into {:,} bytes.".format(
            sample_rate, audio_length_seconds, available_space)

    converted_audio_files = [to_raw_audio(f, sample_rate) for f in tqdm(files)]

    files_with_sizes = [
        (f, len(converted_audio_file))
        for f, converted_audio_file in tqdm(zip(files, converted_audio_files))
    ]
    header = generate_header(files_with_sizes)

    offsets = list(generate_file_offsets(files_with_sizes))
    for (file, offset), (_, size) in zip(offsets, files_with_sizes):
        print (
            "File {} starts at byte {:,} ({}) and "
            "ends at byte {:,} ({})").format(
            file,
            offset,
            as_hex_address(offset + size),
            offset + size,
            as_hex_address(offset + size))

    with open('data/flash_constants.h', 'w') as f:
        for line in generate_c_defines(
            offsets,
            files_with_sizes,
            sample_rate,
            buffer_size
        ):
            f.write(line)
            f.write("\n")

    with open('data/flash_constants.c', 'w') as f:
        for line in generate_c_arrays(offsets, files_with_sizes):
            f.write(line)
            f.write("\n")

    with open(outfile, 'w') as out:
        out.write(header)
        for f in tqdm(converted_audio_files):
            out.write(f)
        size = out.tell()
        # pad with zero bytes until we hit the size of the flash
        out.write("\x00" * (maxsize - size))
    if size > maxsize:
        raise ValueError((
            "Size of requested data ({:,} bytes) is greater "
            "than max flash size ({:,} bytes)!").format(size, maxsize))
    print "Wrote {:,} bytes ({}% of max) to {}.".format(
        size,
        100. * (float(size) / float(maxsize)),
        outfile
    )

if __name__ == "__main__":
    input_folder = sys.argv[-1]
    if not os.path.isdir(input_folder):
        raise ValueError(
            "Usage: %s path_to_directory_with_sorted_audio_files" %
            sys.argv[0])
    flashprep(input_folder)

"""
Usage: flashprep.py path_to_audio_directory

This script will take the audio files in the passed directory,
sort them lexicographically, pick a sample rate to fit them
into the provided memory space, and then output:
  - flash.bin, containing a header plus the audio streams
  - data/flash_constants.h, containing constants
  - data/flash_constants.c, containing an TrackMetadata array
"""

import os
import sys
import math
import struct
from glob import glob
from tqdm import tqdm
import subprocess


SECRET_HEADER_TEXT = """
Hello world! If you're reading this, you've somehow
disassembled this custom Game Boy cartridge and are
reverse engineering it or something. Cool. My name's
Peter, and I built this in 2017/2018. I'd love to
hear from you - send me an email at:
<EMAIL> or find me on most social
networks at @psobot.

What follows is a concatenation of multiple audio
files encoded at 8bits and some arbitrary sample rate.
Their bounds are specified in a rudimentary header format:

uint32_t: number of files
uint32_t: start of file 0
uint32_t: start of file 1...

After the newline at the end of the next sentence,
the header will begin. Happy hacking!
"""

SECRET_HEADER_TEXT = SECRET_HEADER_TEXT.lstrip()
DEFAULT_SIZE_IN_MBITS = 128
MAX_NUMBER_OF_HEX_DIGITS = math.log(DEFAULT_SIZE_IN_MBITS, 2) - 1


def generate_file_offsets(files_with_sizes):
    offset = get_header_size(files_with_sizes)
    for file, size in files_with_sizes:
        yield (file, offset)
        offset += size


def generate_header(files_with_sizes):
    return "".join([
        SECRET_HEADER_TEXT,
        struct.pack("<i", len(files_with_sizes)),
    ] + [
        struct.pack("<i", offset)
        for _, offset
        in generate_file_offsets(files_with_sizes)
    ])


def get_header_table_size(files):
    return 4 + (len(files) * 4)


def get_header_table_start():
    return len(SECRET_HEADER_TEXT)


def get_header_size(files):
    return get_header_table_start() + get_header_table_size(files)


def to_raw_audio(file, sample_rate=None):
    if sample_rate:
        sample_rate_args = ['-ar', str(sample_rate)]
    else:
        sample_rate_args = []

    command = [
        'ffmpeg', '-i', file, '-f', 'u8', '-ac', '1'
    ] + sample_rate_args + ['-']

    return subprocess.check_output(
        command,
        stderr=open(os.devnull, 'w')
    )


def normalize_name(fname):
    return (
        fname
        .split('/')[-1]
        .split('.')[0]
        .upper()
        .replace(' ', '_')
        .replace("(", "")
        .replace(")", ""))


def generate_c_defines(offsets, files_with_sizes, sample_rate, buffer_size):
    yield "#ifndef flash_constants"
    yield "#define flash_constants"
    yield "#define BUFFER_SIZE_BYTE {}".format(buffer_size)
    yield "#define AUDIO_TRACK_COUNT {}".format(len(files_with_sizes))

    yield "#define AUDIO_SAMPLE_RATE {}".format(sample_rate)
    for (file, offset), (_, size) in zip(offsets, files_with_sizes):
        yield "#define AUDIO_{}_START {}".format(normalize_name(file), offset)
        yield "#define AUDIO_{}_END {}".format(
            normalize_name(file), offset + size)

    yield "typedef struct {"
    yield "  unsigned int start_offset;"
    yield "  unsigned int end_offset;"
    yield "} TrackMetadataEntry;"

    yield "TrackMetadataEntry TrackMetadata[AUDIO_TRACK_COUNT];"
    yield ("#define TrackMetadataForLevel(level) "
           "(TrackMetadata[level % AUDIO_TRACK_COUNT])")
    yield "#endif"


def generate_c_arrays(offsets, files_with_sizes):
    yield '#include "flash_constants.h"'

    """
    Note: The original Super Mario Land cart uses this music mapping:

    .musicByLevel
        db 7, 7, 3
        db 8, 8, 5
        db 7, 3, 3
        db 6, 6 ,5
    """

    yield "TrackMetadataEntry TrackMetadata[AUDIO_TRACK_COUNT] = {"
    for (file, offset), (_, size) in zip(offsets, files_with_sizes):
        yield "\t{{ AUDIO_{}_START, AUDIO_{}_END }},".format(
            normalize_name(file),
            normalize_name(file))
    yield "};"


def get_total_audio_length(files):
    # assume original files are at 44100
    sample_rate = 44100
    converted_audio_files = [to_raw_audio(f, sample_rate) for f in tqdm(files)]
    return float(sum(len(file) for file in converted_audio_files)) \
        / float(sample_rate)


def as_hex_address(num):
    return ("0x{:0" + str(int(MAX_NUMBER_OF_HEX_DIGITS)) + "X}").format(num)


def flashprep(
    directory,
    outfile='flash.bin',
    maxsize=DEFAULT_SIZE_IN_MBITS * 1024 * 1024 / 8,
    sample_rate=None,
    buffer_size=512,
):
    """
    Given a list of files, generate one binary with all of them concatenated,
    along with C defines specifying their bounds.
    """
    files = sorted(glob(os.path.join(directory, '*')))

    if sample_rate is None:
        quantize = 50
        available_space = maxsize - get_header_size(files)
        audio_length_seconds = get_total_audio_length(files)
        float_sample_rate = (float(available_space) / audio_length_seconds)
        sample_rate = math.floor(float_sample_rate / quantize) * quantize
        print "Using sample rate of {:,} to fit {:,}s into {:,} bytes.".format(
            sample_rate, audio_length_seconds, available_space)

    converted_audio_files = [to_raw_audio(f, sample_rate) for f in tqdm(files)]

    files_with_sizes = [
        (f, len(converted_audio_file))
        for f, converted_audio_file in tqdm(zip(files, converted_audio_files))
    ]
    header = generate_header(files_with_sizes)

    offsets = list(generate_file_offsets(files_with_sizes))
    for (file, offset), (_, size) in zip(offsets, files_with_sizes):
        print (
            "File {} starts at byte {:,} ({}) and "
            "ends at byte {:,} ({})").format(
            file,
            offset,
            as_hex_address(offset + size),
            offset + size,
            as_hex_address(offset + size))

    with open('data/flash_constants.h', 'w') as f:
        for line in generate_c_defines(
            offsets,
            files_with_sizes,
            sample_rate,
            buffer_size
        ):
            f.write(line)
            f.write("\n")

    with open('data/flash_constants.c', 'w') as f:
        for line in generate_c_arrays(offsets, files_with_sizes):
            f.write(line)
            f.write("\n")

    with open(outfile, 'w') as out:
        out.write(header)
        for f in tqdm(converted_audio_files):
            out.write(f)
        size = out.tell()
        # pad with zero bytes until we hit the size of the flash
        out.write("\x00" * (maxsize - size))
    if size > maxsize:
        raise ValueError((
            "Size of requested data ({:,} bytes) is greater "
            "than max flash size ({:,} bytes)!").format(size, maxsize))
    print "Wrote {:,} bytes ({}% of max) to {}.".format(
        size,
        100. * (float(size) / float(maxsize)),
        outfile
    )

if __name__ == "__main__":
    input_folder = sys.argv[-1]
    if not os.path.isdir(input_folder):
        raise ValueError(
            "Usage: %s path_to_directory_with_sorted_audio_files" %
            sys.argv[0])
    flashprep(input_folder)
Usage: flashprep.py path_to_audio_directory  This script will take the audio files in the passed directory, sort them lexicographically, pick a sample rate to fit them into the provided memory space, and then output:   - flash.bin, containing a header plus the audio streams   - data/flash_constants.h, containing constants   - data/flash_constants.c, containing an TrackMetadata array Hello world! If you're reading this, you've somehow disassembled this custom Game Boy cartridge and are reverse engineering it or something. Cool. My name's Peter, and I built this in 2017/2018. I'd love to hear from you - send me an email at: <EMAIL> or find me on most social networks at @psobot.  What follows is a concatenation of multiple audio files encoded at 8bits and some arbitrary sample rate. Their bounds are specified in a rudimentary header format:  uint32_t: number of files uint32_t: start of file 0 uint32_t: start of file 1...  After the newline at the end of the next sentence, the header will begin. Happy hacking! Note: The original Super Mario Land cart uses this music mapping:      .musicByLevel         db 7, 7, 3         db 8, 8, 5         db 7, 3, 3         db 6, 6 ,5 # assume original files are at 44100 Given a list of files, generate one binary with all of them concatenated,     along with C defines specifying their bounds. # pad with zero bytes until we hit the size of the flash