#!/usr/bin/python

# make_wave.py
#       --copyright--                   Copyright 2007 (C) Tranzoa, Co. All rights reserved.    Warranty: You're free and on your own here. This code is not necessarily up-to-date or of public quality.
#       --url--                         http://www.tranzoa.net/tzpython/
#       --email--                       pycode is the name to send to. tranzoa.com is the place to send to.
#       --bodstamps--
#       August 30, 2006         bar
#       September 2, 2006       bar     --max_sample
#       November 18, 2007       bar     turn on doxygen
#       November 20, 2007       bar     comments
#       November 27, 2007       bar     insert boilerplate copyright
#       May 17, 2008            bar     email adr
#       February 18, 2009       bar     try to keep the ends of a loopable wave a bit better stitched together
#       July 19, 2011           bar     allow the note to be spec'd by letter, etc.
#       --eodstamps--
##      \file
#
#
#       Output a wav file of given frequencies - with the idea that this wave file can be looped.
#
#

import  array
import  math


##                  This logic can make waveforms of 3 types:

##                  Make a sine wave.
SINE_SAMPLES        =   0

##                  Make a triangle, saw-tooth wave.
TRIANGLE_SAMPLES    =   1

##                  Make a square wave.
SQUARE_SAMPLES      =   2


##                  Maximum absolute 16 bit sample value.
LOUD_16_BIT_SAMPLE  =   32767



def make_audio_samples(sample_rate, duration, frequency, form, loudest_sample = LOUD_16_BIT_SAMPLE) :
    """
        Make an array of 16-bit signed audio samples to the given specs.
    """

    samples     = array.array('h')

    sc          = int(sample_rate * duration)
    wsc         = float(sample_rate) / float(frequency)

    if   form  == TRIANGLE_SAMPLES :
        for i in xrange(sc) :
            wi  = math.fmod(float(i), wsc) / wsc
            if  wi <= 0.25 :
                s   = (loudest_sample - 1) * wi * 4.0
            elif wi <= 0.75 :
                s   = -((loudest_sample - 1) * (wi - 0.25) * 4.0) + loudest_sample
            else :
                s   = (loudest_sample * (wi - 0.75) * 4.0) - loudest_sample
            samples.append(int(s))
        pass

    elif form  == SQUARE_SAMPLES :
        for i in xrange(sc) :
            wi  = math.fmod(float(i), wsc) / wsc
            if  wi <= 0.5 :
                samples.append( loudest_sample)
            else :
                samples.append(-loudest_sample)
            pass
        pass

    else :

        for i in xrange(sc) :
            wi  = float(i) / wsc
            samples.append(int(math.sin(2.0 * math.pi * wi) * loudest_sample))
        pass

    return(samples)





def add_samples(samples, max_sample = None) :
    """
        Return an array of the sum of the samples in an array of equal-length arrays of 16-bit signed samples.
    """

    gms                 = max_sample
    if  max_sample     == None :
        max_sample      = 32767

    if  max_sample > 32767 :
        raise "max_sample " + str(max_sample) + " greater than 32767!"

    out_samples         = array.array('h', samples[0])

    ms                  = 0
    for si in xrange(len(out_samples)) :
        s               = out_samples[si]
        for i in xrange(1, len(samples)) :
            s          += samples[i][si]

        ms              = max(ms, abs(s))

        s               = min(s,  max_sample)
        s               = max(s, -max_sample)
        out_samples[si] = s


    if  (gms != None) and (ms > max_sample) :
        mult            = float(max_sample) / float(ms)

        out_samples     = array.array('h', samples[0])

        ms              = 0
        for si in xrange(len(out_samples)) :
            s           = out_samples[si]
            for i in xrange(1, len(samples)) :
                s      += samples[i][si]

            s           = int(s * mult)

            ms          = max(ms, abs(s))

            s           = min(s,  max_sample)
            s           = max(s, -max_sample)
            out_samples[si] = s


        if  ms > max_sample :
            raise "Did not limit samples as requested " + str(ms) + " " + str(max_sample) + " " + str(mult) + "!"

        pass

    return(out_samples)




#
#
#
if __name__ == '__main__' :

    import  sys
    import  wave


    import  replace_file
    import  TZCommandLineAtFile
    import  tzlib
    import  tzmidi



    help_str    = sys.argv[0] + """ (options)   sample_rate     duration_in_seconds     output.wav

--note          MIDI-note                   Include this MIDI note (0..127).
                                            Or note can be of form ^([a-g])\s*([b#]?)\s*(\d*)$
                                                Note(flat|sharp)(octave 1..11 default 6)
                                                From C1..G11.

--loopable                                  Make the output loopable (will be shorter than the duration).

--max_sample    value                       Maximum absolute sample value (1.. default 32767).

--form          sine | triangle | square    Waveform

"""


    sys.argv.pop(0)

    TZCommandLineAtFile.expand_at_sign_command_line_files(sys.argv)


    if  tzlib.array_find(sys.argv, [ "--help", "-h", "/?" ] ) >= 0 :
        print help_str
        sys.exit(254)


    loopable    = False
    form        = SINE_SAMPLES
    notes       = []
    max_sample  = 32767


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--form", "-f" ])
        if  oi < 0 :    break
        del sys.argv[oi]
        form    = sys.argv.pop(oi).lower()
        if  form   == "sine" :
            form    = SINE_SAMPLES
        elif form  == "triangle" :
            form    = TRIANGLE_SAMPLES
        elif form  == "square" :
            form    = SQUARE_SAMPLES
        else :
            print "--form must be sine, triangle or square!"
            sys.exit(254)
        pass


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--note", "-n" ])
        if  oi < 0 :    break
        del sys.argv[oi]
        ns  = sys.argv.pop(oi)
        try :
            notes.append(int(ns))
        except ( ValueError, TypeError ) :
            n   = tzmidi.name_to_midi_note(ns)
            if  n < 0 :
                print "--note must be a MIDI note number or a letter (with optional optional # or b and optional octave 1..11 (C1..G11 default: 6))"
                sys.exit(101)
            notes.append(n)
        pass


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--max_sample", "-m" ])
        if  oi < 0 :    break
        del sys.argv[oi]
        max_sample  = int(sys.argv.pop(oi))


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--loopable", "-l" ])
        if  oi < 0 :    break
        del sys.argv[oi]
        loopable    = True



    if  len(sys.argv) != 3 :
        print "Please tell me the sample rate, the maximum duration, and the output .wav file name!"
        sys.exit(254)

    if  len(notes) == 0 :
        print "Please tell me at least one MIDI note!"
        sys.exit(254)


    sample_rate     = int(sys.argv[0])
    duration        = float(sys.argv[1])
    ofile_name      = sys.argv[2]



    samples         = []

    for n in notes :
        if  n < 0 :
            print "Note %d too low!" % n
            sys.exit(102)
        samples.append(make_audio_samples(sample_rate, duration, tzmidi.midi_note_freq(n), form, LOUD_16_BIT_SAMPLE / len(notes)))


    if  loopable :


        #
        #
        #       Find the best good cut point.
        #           This is the place after the first cycle of the lowest frequency
        #           where all waves are going up,
        #           are below zero,
        #           and the sample furthest from zero is minimized.
        #           (since the waves start going up from zero).
        #
        #

        low_freq        = 1000000.0;
        for n in notes  :
            low_freq    = min(low_freq, tzmidi.midi_note_freq(n))

        si              = int(math.floor(float(sample_rate) / low_freq))
        bi              = si
        bv              = 100000000

        while si < len(samples[0]) :

            v           = 0
            for i in xrange(len(samples)) :
                s       = samples[i][si]
                if  s  >= 0 :
                    v   = 100000000                                 # already up
                    break

                d       = s - samples[i][si - 1]
                if  d   < 0 :
                    v   = 100000000                                 # going down, not up
                    break

                v   = max(v, -s)

            if  bv  > v :
                bv  = v
                bi  = si + 1                                        # include this sample

            si         += 1


        for si in xrange(len(samples)) :
            samples[si] = samples[si][0:bi]
        pass


    data    = add_samples(samples, max_sample)

    if  loopable :
        while len(data) >= 4 :
            d1  = data[1] - data[0]
            # print data[-2], data[-1], data[0], data[1], d1, abs(d1 - (data[0] - data[-1])), abs(d1 - (data[0] - data[-2]))
            if  abs(d1 - (data[0] - data[-1])) <= abs(d1 - (data[0] - data[-2])) :
                break
            data.pop()                                              # here, we're keeping a loopable thing from having a zero at both ends
        pass

    data    = data.tostring()

    tfname  = ofile_name + ".tmp"

    fo      = wave.open(tfname, "wb")

    fo.setnchannels(1)
    fo.setsampwidth(2)
    fo.setframerate(sample_rate)

    fo.writeframes(data)

    fo.close()

    replace_file.replace_file(ofile_name, tfname, ofile_name + ".bak")





##      Public things.
__ALL__ = [
            'make_audio_samples',
            'add_samples',

            'SINE_SAMPLES',
            'TRIANGLE_SAMPLES',
            'SQUARE_SAMPLES',

            'LOUD_16_BIT_SAMPLE',
          ]

#
#
#
# eof

