#!/usr/bin/env python3

Generate samples.

import os
import sys
import traceback
import struct
import random
from utils import log, err

def to_bytes(f):
    return struct.pack('h', int(f * (2**15 - 1)))

def gen(fun, samplerate, volume):
    t = 0
    while True:
        for i in range(samplerate):
            j = i / samplerate
            yield to_bytes(fun(j, t) * volume)
            t += 1

def parse_arg(s):
        key, val = s.split('=', 1)
    except ValueError:
        return {s: True}

    funs = {
        'seed': int,
        'samplerate': float,
        'volume': int
    conv = funs.get(key, lambda x: x)
    return {key: conv(val)}

def import_module(name):
    # This is the easiest way.
    exec('import {}'.format(name))
    return eval(name)

class FunctionGetterDict(dict):
    'get' is lazy with its fallback parameter.
    def get(self, key, f=None):
            return self.__getitem__(key)
        except KeyError:
                return f()
            except TypeError:
                return f

def main(*args):
    rargs = []
    settings = FunctionGetterDict()
    for arg in args:
        if arg.startswith('-'):

    if settings.get('quiet'):
        sys.stderr = open(os.devnull, 'w')

    if settings.get('version'):
        return 0

        module_fun = rargs[0]
    except IndexError:
        if settings.get('help'):
            return 0
            err('missing module and function name')
            return 1

        module_name, fun_name = module_fun.split('.', 1)
    except ValueError:
            module = import_module(module_fun)
        except ImportError:
            err('module {} invalid'.format(repr(module_fun)))
            return 1
        if settings.get('help'):
            log(get_help(module, just_module=True))
            return 0
            err('missing function name')
            return 1

        module = import_module(module_name)
    except ImportError:
        err('module {} invalid'.format(repr(module_name)))
        return 1

        fun_gen = module.__getattribute__(fun_name)
    except AttributeError:
        err('function {} does not exist'.format(
            repr(module_name + '.' + fun_name)))
        return 1

    fun_args = rargs[1:]

    if not 'seed' in settings:
        settings['seed'] = random.getrandbits(64)
    seed = settings['seed']
    log('seed is:', seed)

    if settings.get('help'):
        return 0

    if not 'samplerate' in settings:
        settings['samplerate'] = 44100
    if not 'volume' in settings:
        settings['volume'] = 1

        fun = fun_gen(*fun_args, **settings)
    except TypeError as e:
        return 1

        with open(1, 'wb') as out:
            for some_bytes in gen(fun, settings['samplerate'], settings['volume']):
    except (KeyboardInterrupt, BrokenPipeError):
        return 0

def get_version():
    return '''\
sample: generate raw sound data with Python 3
version: git
website: http://metanohi.name/projects/sample
license: WTFPL 2.0

def get_help(fun=None, just_module=False):
    if fun is None:
        return '''\
sample: generate raw sound data with Python 3

Usage: sample [<option>...] <module>.<function> [<arg>...]

'sample' is called with optional settings, a required module function name, and
possibly arguments for that function.  When 'sample' is run, it does two things:
  + outputs a signed 16 bits little endian mono byte stream to standard out
  + prints details and errors to standard error

NOTE: 'sample' does not use numpy or any external Python library, so it's slower
than it needs to be.  On the upside, you don't have to read up on any library

NOTE: 'sample' is naive w.r.t. its command-line input, so don't make others run
the program through your user, as they can probably break out quite easily.

An option starts with a '-' character and does one of two things:
  + assigns a value to a key: '-<key>=<value>'
  + sets a key to be true: '-<key>'

Some options are global, while some may only affect certain functions.  The
global options are:

  -seed=<int>    Set the seed for the random generator.  This enables
                 reproducibility for otherwise random functions.  The default
                 is to let Python pick one.
  -samplerate=<int>    Set the number of samples per second.  The default is
  -volume=<float>    Set the global volume; must be a number between 0 and 1.
                     The default is 1.
  -quiet    Do not log anything.
  -help    Print help for a function.  If only the module is given ('<module>'
           instead of '<module>.<function>', try to find all applicable
           functions for that module, and print the help text for each function.
           This is done by looking for a '__sample_functions__' variable
           containing functions.  If no function or module is given, print this
           very help text.  Exit after printing.
  -version    Print short version information and exit.

Apart from 'sample', you can also use these scripts (which all have external
dependencies; see the scripts for full documentation):
  + 'play': sends sound data to your speaker.  Works independently of 'sample'.
  + 'save': records to a file.  Works independently of 'sample'.
  + 'sampleplay': uses 'play', but with the arguments for 'sample'.
  + 'samplesave': uses 'save', but with the arguments for 'sample'.

'sample' is distributed with a small module called 'base' of simple primitives.


  Make a sine curve of 600 Hz and play it:
    sampleplay base.sin 600

  Save that same curve to a file:
    samplesave music.wav base.sin 600

  Find out which functions are available in the 'base' module:
    sample -help base

  Check out the details of the sine function:
    sample -help base.sin

  Make three overlapping sines (900 Hz, 95 Hz, and 380 Hz) and play them:
    sampleplay base.sins 900 95 380

  Play 5 sine curves with random frequencies, phases, and amplitudes:
    sampleplay base.random_sins 5

  Do the same, but use a seed for reproducibility's sake:
    sampleplay -seed=12123639752121693971 base.random_sins 5
        # It's mostly the same code even if just_module is true.
        name = fun.__name__
        doc = fun.__doc__
        if doc is None:
            return '{}: no documentation'.format(repr(name))
            doc = doc.strip()
            text = '{}:\n{}'.format(repr(name), doc)

            if not just_module:
                return text
                module = fun
                    funs = module.__getattribute__('__sample_functions__')
                except AttributeError:
                    text += '\n\nNo public functions.'
                    return text
                text += '\n\nAvailable functions:'
                for fun in funs:
                    text += '\n\n{}'.format(get_help(fun))
                return text

if __name__ == '__main__':