#!/usr/bin/python2.3


#
#   Copyright (c) 2001-2002 Alexander Leidinger. All rights reserved.
#
#   Redistribution and use in source and binary forms, with or without
#   modification, are permitted provided that the following conditions
#   are met:
#
#   1. Redistributions of source code must retain the above copyright
#      notice, this list of conditions and the following disclaimer.
#   2. Redistributions in binary form must reproduce the above copyright
#      notice, this list of conditions and the following disclaimer in the
#      documentation and/or other materials provided with the distribution.
#
#   THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
#   ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
#   ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
#   FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
#   DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
#   OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
#   HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
#   LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
#   OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
#   SUCH DAMAGE.
#

# $Id: slame,v 1.5 2003/06/17 11:03:08 aleidinger Exp $

# slame:
# quick hack to test some aspects of pylame, a cleanup is needed

# TODO:
#  * merge use_gui and have_gtk
#  * split into multiple files
#  * cleanup

import os, sys, getopt
import wave, aifc, sunau
import lame

try:
    import gtk, threading#, time
    have_gtk = 1
    data_timeout  = 400
    event_timeout = .5
except 'ImportError':
    have_gtk = 0

    
def usage():
    print 'Usage: slame [-b <bitrate>] [-achqv] <infile> [<outfile>]' + os.linesep + \
          '       -a: produce an ABR MP3'                    + os.linesep + \
	  '       -b: use bitrate of <bitrate> kbps'         + os.linesep + \
          '       -c: produce a CBR MP3'                     + os.linesep + \
	  '       -g: use a GUI to display the status'       + os.linesep + \
          '       -h: print this help'                       + os.linesep + \
          '       -q: be quiet (no output except on errors)' + os.linesep + \
          '       -v: produce a more verbose output'         + os.linesep + \
	  '   * per default a VBR MP3 is created'            + os.linesep + \
	  '   * the GUI (-g) is only displayed if GTK+ is available' + os.linesep + \
	  '   * if no <outfile> is given, slame tries to guess a reasonable one'


# What the f..., why isn't the GUI updated smoother?
# Even my previous hack was smoother. :-(
# Pseudeo threading withhin python?
#
# After adding 'data_event' (sort of manual thread switching, ugly :-( )
# it's smoother now.
def update_gtk_gui():
    global progressbar1, label_bitrate, last_bitrate, actual_bitrate, \
           last_progress, actual_progress, data_lock, abort, data_timeout, \
	   data_event

    data_lock.acquire()
    if last_bitrate != actual_bitrate:
	last_bitrate = actual_bitrate
	text_bitrate = '%5.1f kbps' % (last_bitrate)
	label_bitrate.set_text(text_bitrate)
    if last_progress != actual_progress:
	progressbar1.update(actual_progress)
	last_progress = actual_progress

    data_event.set()
	
    if 0 != abort:
	data_lock.release()
	gtk.mainquit()
    else:
	data_lock.release()
	gtk.timeout_add(data_timeout, update_gtk_gui)

    
def gui_quit(a):
    global abort
    abort = 2
	
    
def init_gtk(in_filename, out_filename, vbr, bitrate):
    global label_bitrate, progressbar1, data_timeout
    
    window1 = gtk.GtkWindow()
    window1.connect('destroy', gui_quit)
    window1.set_title('sLAME')
    window1.set_border_width(3)
    
    vbox1 = gtk.GtkVBox(0, 0)
    window1.add(vbox1)
    
    frame1 = gtk.GtkFrame()
    frame1.set_label('Files')
    vbox1.add(frame1)
    
    table1 = gtk.GtkTable(2, 2, 0)
    table1.set_row_spacings(3)
    table1.set_col_spacings(3)
    table1.set_border_width(3)
    frame1.add(table1)

    # What the f..., why aren't the labels adjusted?
    label6 = gtk.GtkLabel('Input:')
    label6.set_justify(GTK.JUSTIFY_RIGHT)
    table1.attach(label6, 0, 1, 0, 1)
    
    label_in_name = gtk.GtkLabel(in_filename)
    label_in_name.set_justify(GTK.JUSTIFY_LEFT)
    table1.attach(label_in_name, 1, 2, 0, 1, GTK.EXPAND | GTK.FILL)
    
    label7 = gtk.GtkLabel('Output:')
    label7.set_justify(GTK.JUSTIFY_RIGHT)
    table1.attach(label7, 0, 1, 1, 2, GTK.FILL)

    label_out_name = gtk.GtkLabel(out_filename)
    label_out_name.set_justify(GTK.JUSTIFY_LEFT)
    table1.attach(label_out_name, 1, 2, 1, 2, GTK.EXPAND | GTK.FILL)
    
    frame2 = gtk.GtkFrame()
    frame2.set_label('Status')
    vbox1.add(frame2)

    table2 = gtk.GtkTable(2, 4, 0)
    table2.set_row_spacings(3)
    table2.set_col_spacings(3)
    table2.set_border_width(3)
    frame2.add(table2)
    
    label8 = gtk.GtkLabel('Type:')
    label8.set_justify(GTK.JUSTIFY_RIGHT)
    table2.attach(label8, 0, 1, 0, 1, GTK.FILL)
    
    if lame.VBR_DEFAULT == vbr:
	mp3_type = 'VBR'
    elif lame.VBR_ABR == vbr:
	mp3_type = 'ABR'
    elif lame.VBR_OFF == vbr:
	mp3_type = 'CBR'
    else:
	mp3_type = 'Unknown'

    label_mp3_type = gtk.GtkLabel(mp3_type)
    label_mp3_type.set_justify(GTK.JUSTIFY_LEFT)
    table2.attach(label_mp3_type, 1, 2, 0, 1, GTK.FILL)
    
    label10 = gtk.GtkLabel('Bitrate:')
    label10.set_justify(GTK.JUSTIFY_RIGHT)
    table2.attach(label10, 2, 3, 0, 1, GTK.FILL)
    
    label_bitrate = gtk.GtkLabel(str(bitrate) + " kbps")
    label_bitrate.set_justify(GTK.JUSTIFY_LEFT)
    table2.attach(label_bitrate, 3, 4, 0, 1, GTK.FILL)
    
    label9 = gtk.GtkLabel('Progress:')
    label9.set_justify(GTK.JUSTIFY_RIGHT)
    table2.attach(label9, 0, 1, 1, 2, GTK.FILL)
    
    progressbar1 = gtk.GtkProgressBar()
    progressbar1.set_bar_style(GTK.PROGRESS_CONTINUOUS)
    progressbar1.set_orientation(GTK.PROGRESS_LEFT_TO_RIGHT)
    progressbar1.update(0.0)
    progressbar1.set_show_text(1)
    progressbar1.set_format_string('%P %%')
    table2.attach(progressbar1, 1, 4, 1, 2, GTK.EXPAND | GTK.FILL)

    gtk.timeout_add(data_timeout, update_gtk_gui)
    
    window1.show_all()

    gtk.mainloop()

    
def set_parameters(mp3, nchannels, samplerate, nframes, bitrate, vbr):
    mp3.set_num_channels(nchannels)
    mp3.set_in_samplerate(samplerate)
    mp3.set_num_samples(long(nframes))

    mp3.set_vbr(vbr)
    if 8 <= bitrate <= 320:
	mp3.set_bitrate(bitrate)
    mp3.set_preset(bitrate)
    return mp3


def get_defaults():
    # defaults:
    bitrate = lame.PRESET_STANDARD
    verbose = 0
    quiet   = 0
    vbr     = lame.VBR_DEFAULT
    use_gui = 0

    return bitrate, verbose, quiet, vbr, use_gui


def process_options_or_exit(arglist):
    try:
        optlist, args = getopt.getopt(arglist, 'ab:cghqv')
    except getopt.GetoptError:
        usage()
        sys.exit(1)

    bitrate, verbose, quiet, vbr, use_gui = get_defaults()

    # process options
    for opt, val in optlist:
        if opt == '-a':
            vbr     = lame.VBR_ABR
	    continue
	if opt == '-b':
	    bitrate = int(val)
	    continue
	if opt == '-c':
	    vbr     = lame.VBR_OFF
	    continue
	if opt == '-g':
	    use_gui = 1
	    print "GUI is disabled for now..."
	    sys.exit(1)
	    continue
	if opt == '-h':
	    usage()
	    sys.exit(0)
	    continue            # can't happen, but makes the editor happy
        if opt == '-q':
            quiet = 1
            continue
        if opt == '-v':
            verbose = 1
            continue

    # check number of remaining arguments
    if (1 > len(args)) | (2 < len(args)):
        usage()
        sys.exit(1)

    in_file  = os.path.normcase(os.path.normpath(os.path.expanduser(args[0])))
    if 2 == len(args):
	mp3_name = os.path.normcase(os.path.normpath(os.path.expanduser(args[1])))
    else:
	root, extension = os.path.splitext(in_file)
	mp3_name = root + '.mp3'
    
    return args, bitrate, verbose, quiet, vbr, use_gui, in_file, mp3_name


def open_soundfile_or_exit(file):
    try:
        sound = wave.open(file, 'rb')
    except wave.Error, wave_errval:
        try:
            sound = aifc.open(file, 'rb')
        except aifc.Error, aifc_errval:
            try:
                sound = sunau.open(file, 'rb')
            except sunau.Error, sunau_errval:
                print 'Unknown file format:' + os.linesep + \
                      ' AIFC  :', aifc_errval,  os.linesep, \
                      ' SUN AU:', sunau_errval, os.linesep, \
                      ' WAVE  :', wave_errval,  os.linesep
                sys.exit(1)
                  
    return sound


def is_readable_or_exit(file):
    if 1 != os.access(file, os.R_OK):
        print 'File ' + file + ' not readable.'
        sys.exit(1)


def get_average(bitrate_hist, frames_processed):
    average = float(0)
    for i in range(0, 14):
	if 0 != bitrate_hist[i]["value"]:
	    average += bitrate_hist[i]["bitrate"] \
	               * bitrate_hist[i]["value"] \
		       / float(frames_processed)
    
    return average


def print_stats(mp3, processed_bytes, raw_size, verbose, vbr, bitrate, use_gui):
    global actual_bitrate, actual_progress, last_bitrate, last_progress, \
           data_lock, have_gtk, data_event, event_timeout
    
    bitrate_hist = mp3.get_bitrate_histogram()
    stmode_hist  = mp3.get_bitrate_stereo_mode_histogram()
    frames_processed = mp3.get_frame_num()
    frames_total     = mp3.get_total_frames()
    stmode_total     = mp3.get_stereo_mode_histogram()

    if lame.VBR_DEFAULT == vbr:
	average_rate = get_average(bitrate_hist, frames_processed)
    else:
	average_rate = bitrate
	
    progress = frames_processed/float(frames_total)
	
    if (1 == have_gtk) & (1 == use_gui):
	data_event.wait(event_timeout)
	if data_event.isSet():
	    data_event.clear()
	data_lock.acquire()
	actual_bitrate  = average_rate
	actual_progress = progress
	data_lock.release()
    else:
	if 1 == verbose:
	    print ''
	
	if lame.VBR_DEFAULT == vbr:
	    print '\r%9d of %9d bytes (%04.1f%%, %.1f kbps)' %      (
	          processed_bytes, raw_size,
		  float(processed_bytes)/raw_size*100,
		  average_rate ),
	else:
	    # ABR & CBR
	    print '\r%9d of %9d bytes' %    (
	          processed_bytes, raw_size ),
	
	sys.stdout.flush()
		  
	if 1 == verbose:
	    print ''
	    for i in range(0, 14):
		print '%3d: %4d (%5.1f%%)  LR: %4d LR-I: %4d MS: %4d MS-I: %4d' % (
		      bitrate_hist[i]["bitrate"], bitrate_hist[i]["value"],
		      100*bitrate_hist[i]["value"]/float(frames_total),
		      stmode_hist[i]["LR"], stmode_hist[i]["LR-I"],
		      stmode_hist[i]["MS"], stmode_hist[i]["MS-I"]                )

	    print 'Total: %4d of %4d frames (%5.1f%%)  LR: %4d LR-I: %4d MS: %4d MS-I: %4d' % (
		  frames_processed, frames_total, progress*100,
		  stmode_total["LR"], stmode_total["LR-I"],
		  stmode_total["MS"], stmode_total["MS-I"]  )


def main():
    global actual_bitrate, actual_progress, last_bitrate, last_progress, \
           data_lock, gui_thread, abort, have_gtk, data_event
    
    # argument parsing
    args, bitrate, verbose, quiet, vbr, use_gui, in_file, mp3_name = \
        process_options_or_exit(sys.argv[1:])

    is_readable_or_exit(in_file)

    sound = open_soundfile_or_exit(in_file)
    
    # get some info about the infile
    nchannels, sampwidth, samplerate, nframes, comptype, compname = \
        sound.getparams()

    raw_size = nchannels * sampwidth * nframes

    if 1 == verbose & 0 == quiet:
        print "File      :", in_file
        print "nchannels :", nchannels
        print "sampwidth :", sampwidth
        print "samplerate:", samplerate
        print "nframes   :", nframes
        print "comptype  :", comptype
        print "compname  :", compname
        print "raw size  :", raw_size

        print os.linesep + "Lame:"
        print "URL      :", lame.url()
        print "Version  :", lame.version()

    if 2 != sampwidth:
        print 'Sorry, no support for != 16bit samples.'
        sys.exit(1)

    # mp3file
    mp3 = lame.init()
    mp3_file = open(mp3_name, "wb+")
    
    mp3 = set_parameters(mp3, nchannels, samplerate, nframes, bitrate, vbr)
    mp3.init_parameters()

    abort = 0
    if (0 == quiet) & (1 == have_gtk) & (1 == use_gui):
	# XXX TODO: put into seperate function
	last_bitrate    = bitrate
	last_progress   = 0.0
	actual_bitrate  = bitrate
	actual_progress = 0.0
	data_lock  = threading.Lock()
	data_event = threading.Event()
	gui_thread = threading.Thread(name='GUI', target=init_gtk, args=(in_file, mp3_name, vbr, bitrate))
	gui_thread.start()

    num_samples_per_enc_run = samplerate

    # 1 sample = 2 bytes
    num_bytes_per_enc_run = nchannels * num_samples_per_enc_run * sampwidth
    
    processed_bytes = 0
    while 0 == abort:
        frames = sound.readframes(num_samples_per_enc_run)
        condition = num_bytes_per_enc_run != len(frames)
	if 1 == condition:
	    abort = condition
        if 2 == nchannels:
	    data = mp3.encode_interleaved(frames)
	    mp3_file.write(data)
        else:
            print 'only 2 channels at the moment!'
            sys.exit(1)

        processed_bytes += len(frames)

	if 0 == quiet:
	    print_stats(mp3, processed_bytes, raw_size, verbose, vbr, bitrate, use_gui)

    data = mp3.flush_buffers()
    mp3_file.write(data)

    mp3.write_tags(mp3_file)

    if 0 == quiet:
	print_stats(mp3, processed_bytes, raw_size, verbose, vbr, bitrate, use_gui)

    mp3_file.close()
    mp3.delete()
    sound.close()

    
if __name__ == '__main__':
    main()
