#!/usr/bin/env python
# encoding: utf-8
"""
bigeval.py

Created by <NAME> on 2010-07-02.
Copyright (c) 2010 The Echo Nest Corporation. All rights reserved.
"""

import getopt
import sys
import os
import time
import socket
import subprocess
try:
    import json
except ImportError:
    import simplejson as json
import pyechonest.config as config
import pyechonest.song as song
import random
import math
sys.path.insert(0, "../API")
import fp

config.CODEGEN_BINARY_OVERRIDE = os.path.abspath("../../echoprint-codegen/echoprint-codegen")

_local_bigeval = {}
_new_music_files = []
_new_queries = 0
_old_queries = 0
_total_queries = 0

def decode_to_wav(decoder, file, target, what, start=-1, duration=-1, volume=100, downsample_to_22 = False, channels=2, speed_up=False, slow_down=False):
    """ Given a file, a decoder and bunch of munge parameters, munge the file using the given decoder """

    # We only need to do this once, regardless of decoder
    if start > 0: what.update({"start":start})
    if duration > 0: what.update({"duration":duration})
    if volume > 0: what.update({"volume":volume})
    if downsample_to_22: what.update({"downsample_22":True})
    if channels < 2: what.update({"mono":True})
    # speed_up and slow_down totally both break the FP. This is OK and expected but we should test it anyway.
    if(speed_up): what.update({"skip_every_other_frame":True})
    if(slow_down): what.update({"play_every_frame_2x":True})

    if decoder == 'mpg123':
        cmd = ["mpg123", "-q", "-w", target]
        if start > 0: cmd.extend(["-k", str(int(start*44100 / 1152))])
        if duration > 0: cmd.extend(["-n", str(int(duration*44100 / 1152))])
        if volume > 0: cmd.extend(["-f", str(int( (float(volume)/100.0) * 32768.0 ))])
        if downsample_to_22: cmd.extend(["-2"])
        if channels < 2: cmd.extend(["-0"])
        if speed_up: cmd.extend(["-d", "2"])
        if slow_down: cmd.extend(["-h", "2"])
        cmd.append(file)
    elif decoder =="ffmpeg":
        cmd = ["ffmpeg", "-i", file, "-f", "wav", "-y"]
        if start > 0: cmd.extend(["-ss", str(start)])
        if duration > 0: cmd.extend(["-t", str(duration)])
        #if volume > 0: cmd.extend(["-vol", str(int( (float(volume)/100.0) * 32768.0 ))]
        # -vol is undocumented, but apparently 256 is "normal"
        if downsample_to_22: cmd.extend(["-ar", "22050"])
        if channels < 2: cmd.extend(["-ac", "1"])
        #if speed_up: cmd.extend(["-d", "2"])
        #if slow_down: cmd.extend(["-h", "2"])
        cmd.append(target)
    elif decoder == "mad":
        cmd = ["madplay", "-Q", "-o", "wave:%s" % target]
        if start > 0: cmd.extend(["-s", str(start)])
        if duration > 0: cmd.extend(["-t", str(duration)])
        #if volume > 0: cmd.extend(["-f", str(int( (float(volume)/100.0) * 32768.0 ))]
        # --attenuate or --amplify takes in dB -> need to convert % to db
        if downsample_to_22: cmd.extend("--downsample")
        if channels < 2: cmd.extend(["-m"])
        #if speed_up: cmd.extend(["-d", "2"])
        #if slow_down: cmd.extend(["-h", "2"])
        cmd.append(file)
    elif decoder == "sox":
        cmd = ["sox", file, target]
        if start < 0: start = 0
        cmd.extend(["trim", str(start)])
        if duration > 0: cmd.extend([str(duration)])
        #if volume > 0: cmd.extend()
        if downsample_to_22: cmd.extend(["rate", "22050"])
        if channels < 2: cmd.extend(["channels", "1"])
        if speed_up: cmd.extend(["speed", "2.0"])
        if slow_down: cmd.extend(["speed", "0.5"])
    else:
        return (what, None)
    """
    # These will decode an mp3 to wav, but can't handle start/duration parameters
    elif decoder == "lame":
        cmd = ["lame", "--decode", file, target]
    elif decoder == "afconvert":
        cmd = ["afconvert", "-f", "WAVE", "-d", "LEI16", file, target]
    """
    subprocess.Popen(cmd, stderr=subprocess.PIPE).communicate()
    return (what, target)

def munge(file, start=-1, duration=-1, bitrate=128, volume=-1, downsample_to_22 = False, speed_up = False, slow_down = False, lowpass_freq = -1, encode_to="mp3", decoder="mpg123", channels=2):
    """
        duration: seconds of source file
        start: seconds to start reading
        volume 0-100, percentage of original
        bitrate: 8, 16, 32, 64, 96, 128, 160, 192, 256, 320
        downsample_to_22: True or False
        speed_up: True or False
        slow_down: True or False
        lowpass_freq: -1 (don't), 22050, 16000, 12000, 8000, 4000
        encode_to: mp3 (uses LAME), m4a (uses ffmpeg), wav, ogg (uses ffmpeg) (ogg does not work yet)
        channels: 1 or 2
    """
    
    # Get a tempfile to munge to
    me = "/tmp/temp_"+str(random.randint(1,32768))+".wav"
    what = {"decoder": decoder}

    (what, me) = decode_to_wav(decoder, file, me, what, start, duration, volume, downsample_to_22, channels, speed_up, slow_down)

    if not os.path.exists(me):
        print >> sys.stderr, "munge result not there"
        return (None, what)
        
    file_size = os.path.getsize(me)
    if(file_size<100000):
        print >> sys.stderr, "munge too small"
        os.remove(me)
        return (None, what)

    roughly_in_seconds = file_size / 176000
    what.update({"actual_file_length":roughly_in_seconds})

    if encode_to == "wav":
        what.update({"encoder":"none","encode_to":"wav"})
        return (me, what)

    if encode_to == "mp3":
        what.update({"encoder":"lame","encode_to":"mp3"})
        cmd = "lame --silent -cbr -b " + str(bitrate) + " "
        if(lowpass_freq > 0):
            what.update({"lowpass":lowpass_freq})
            cmd = cmd + " --lowpass " + str(lowpass_freq) + " "
        what.update({"bitrate":bitrate})
        cmd = cmd + me + " " + me + ".mp3"
    
    if encode_to == "m4a":
        what.update({"encoder":"ffmpeg","encode_to":"m4a"})
        cmd = "ffmpeg -i " + me + " -ab " + str(bitrate) + "k " + me + ".m4a 2>/dev/null"

    # NB ogg does not work on my copy of ffmpeg...
    if encode_to == "ogg":
        what.update({"encoder":"ffmpeg","encode_to":"ogg"})
        cmd = "ffmpeg -i " + me + " -ab " + str(bitrate) + "k " + me + ".ogg 2>/dev/null"

    os.system(cmd)
    try:
        os.remove(me)
    except OSError:
        return (None, what)
    return (me+"."+encode_to, what)

def prf(numbers_dict):
    # compute precision, recall, F, etc
    precision = recall = f = true_negative_rate = accuracy = 0
    tp = float(numbers_dict["tp"])
    tn = float(numbers_dict["tn"])
    fp = float(numbers_dict["fp-a"]) + float(numbers_dict["fp-b"])
    fn = float(numbers_dict["fn"])
    if tp or fp:
        precision = tp / (tp + fp)
    if fn or tp:
        recall = tp / (tp + fn)
    if precision or recall:
        f = 2.0 * (precision * recall)/(precision + recall)
    if tn or fp:
        true_negative_rate = tn / (tn + fp)
    if tp or tn or fp or fn:
        accuracy = (tp+tn) / (tp + tn + fp + fn)
    print "P %2.4f R %2.4f F %2.4f TNR %2.4f Acc %2.4f %s" % (precision, recall, f, true_negative_rate, accuracy, str(numbers_dict))
    return {"precision":precision, "recall":recall, "f":f, "true_negative_rate":true_negative_rate, "accuracy":accuracy}

def dpwe(numbers_dict):
    # compute dan's measures.. probability of error, false accept rate, correct accept rate, false reject rate
    car = far = frr = pr = 0
    r1 = float(numbers_dict["tp"])
    r2 = float(numbers_dict["fp-a"])
    r3 = float(numbers_dict["fn"])
    r4 = float(numbers_dict["fp-b"])
    r5 = float(numbers_dict["tn"])
    if r1 or r2 or r3:
        car = r1 / (r1 + r2 + r3)
    if r4 or r5:
        far = r4 / (r4 + r5)
    if r1 or r2 or r3:
        frr = (r2 + r3) / (r1 + r2 + r3)
    # probability of error
    pr = ((_old_queries / _total_queries) * frr) + ((_new_queries / _total_queries) * far)    
    print "PR %2.4f CAR %2.4f FAR %2.4f FRR %2.4f %s" % (pr, car, far, frr, str(numbers_dict))
    stats = {}
    stats.update(numbers_dict)    
    dpwe_nums = {"pr":pr, "car": car, "far":far, "frr":frr}
    stats.update(dpwe_nums)
    return dpwe_nums

def test_file(filename, local = False, expect_match=True, original_TRID=None, remove_file = True):
    """
        Test a single file. This will return a code like tn, tp, fp, err, etc
    """
    matching_TRID = None
    if filename is None:
        return "err-munge" # most likely a munge error (file too short, unreadable, etc)
    try:
        # don't pass start and duration to codegen, assume the munging takes care of codelength
        if not local:
            query_obj = song.util.codegen(filename, start=-1, duration=-1)
            s = fp.best_match_for_query(query_obj[0]["code"])
            if s.TRID is not None:
                matching_TRID = s.TRID
        else:
            query_obj = song.util.codegen(filename, start=-1, duration=-1)
            s = fp.best_match_for_query(query_obj[0]["code"], local=local)
            if s.TRID is not None:
                matching_TRID = s.TRID

    except IOError:
        print "TIMEOUT from API server"
        return "err-api"
    except TypeError: # codegen returned none
        return "err-codegen"
    if remove_file:
        if os.path.exists(filename):
            os.remove(filename)
        else:
            return "err-munge"

    # if is not None there was a response
    if s is not None:
        # there was a match
        if len(s) > 0:
            # if we're expecting a match, check that it's the right one
            if expect_match:
                if original_TRID == matching_TRID:
                    # we expected a match, it's the right one. TP
                    return "tp"
                else:
                    # we expected a match but it's the wrong one. FP-a
                    return "fp-a"
            else:
                # we did not expect a match. FP-b
                return "fp-b"
        else:
            # there was no match from the API
            if expect_match:
                # we expected a match. FN
                return "fn"
            else:
                # we did not expect a match. TN
                return "tn"
    else:
        # s is None, that means API error-- almost definitely codegen returned nothing.
        return "err-codegen"

def test_single(filename, local=False, **munge_kwargs):
    """
        Perform a test on a single file. Prints more diagnostic information than usual.
    """
    (new_file, what) = munge(filename, **munge_kwargs)
    query_obj = song.util.codegen(new_file, start=-1, duration=-1)
    s = fp.best_match_for_query(query_obj[0]["code"],local=local)
    if s.TRID is not None:
        if local:
            metad = _local_bigeval[s.TRID]
        else:
            metad = fp.metadata_for_track_id(s.TRID)
            metad["title"] = metad["track"]
        song_metadata = {"artist": metad.get("artist", ""), "release": metad.get("release", ""), "title": metad.get("title", "")}
        print str(song_metadata)
    else:
        print "No match"
    
    decoded = fp.decode_code_string(query_obj[0]["code"])
    print str(len(decoded.split(" "))/2) + " codes in original"
    response = fp.query_fp(decoded, local=local, rows=15)
    if response is not None:
        print "From FP flat:"
        tracks = {}
        scores = {}
        for r in response.results:
            trid = r["track_id"].split("-")[0]
            if local:
                metad = _local_bigeval[trid]
            else:
                metad = fp.metadata_for_track_id(trid)
                metad["title"] = metad["track"]
            m = {"artist": metad.get("artist", ""), "release": metad.get("release", ""), "title": metad.get("title", "")}
            if m is not None:
                actual_match = fp.actual_matches(decoded, fp.fp_code_for_track_id(r["track_id"], local=local))
                tracks[r["track_id"]] = (m, r["score"], actual_match)
                scores[r["track_id"]] = actual_match
            else:
                print "problem getting metadata for " + r["track_id"]
        sorted_scores = sorted(scores.iteritems(), key=lambda (k,v): (v,k), reverse=True)
        for (trackid, score) in sorted_scores:
            (m, score, actual_match) = tracks[trackid]
            print trackid + " (" + str(int(score)) + ", " + str(actual_match) +") - " + m["artist"] + " - " + m["title"]
    else:
        print "response from fp flat was None -- decoded code was " + str(decoded)
    os.remove(new_file)

def test(how_many, diag=False, local=False, no_shuffle=False, **munge_kwargs):
    """
        Perform a test. Takes both new files and old files, munges them, tests the FP with them, computes various measures.
        how_many: how many files to process
        munge_kwargs: you can pass any munge parameter, like duration=30 or volume=50
    """
    results = {"fp-a":0, "fp-b":0, "fn":0, "tp":0, "tn":0, "err-codegen":0, "err-munge":0, "err-api":0, "err-data":0, "total":0}
    
    docs_to_test = _local_bigeval.keys() + _new_music_files
    if not no_shuffle:
        random.shuffle(docs_to_test)

    for x in docs_to_test:
        if results["total"] == how_many:
            return results

        if x.startswith("TR"): # this is a existing TRID
            original_TRID = x
            filename = _local_bigeval[x]["filename"]
            (new_file, what) = munge(filename, **munge_kwargs)
            result = test_file(new_file, expect_match = True, original_TRID = x, local=local)
                
        else: # this is a new file
            filename = x
            original_TRID = None
            (new_file, what) = munge(filename, **munge_kwargs)
            result = test_file(new_file, expect_match = False, local=local)
        
        if result is not "tp" and result is not "tn":
            print "BAD ### " + filename + " ### " + result + " ### " + str(original_TRID) + " ### " + str(what)
            if diag and not result.startswith("err"):
                test_single(filename, local=local, **munge_kwargs)
            
        results[result] += 1
        results["total"] += 1

        if results["total"] % 10 == 0:
            dpwe(results)

    return results
    
def usage():
    print "FP bigeval"
    print "\t-1\t--single  \tSingle mode, given a filename show an answer"
    print "\t-c\t--count   \tHow many files to process (required if not --single)"
    print "\t-s\t--start   \tIn seconds, when to start decoding (0)"
    print "\t-d\t--duration\tIn seconds, how long to decode, -1 is unchanged (30)"
    print "\t-D\t--decoder \tWhat decoder to use to make pcm ([mpg123|ffmpeg|sox|mad])"
    print "\t-b\t--bitrate \tIn kbps, encoded bitrate. only for mp3, m4a, ogg. (128)"
    print "\t-v\t--volume  \tIn %, volume of original. -1 for no adjustment. (-1)"
    print "\t-l\t--lowpass \tIn Hz, lowpass filter. -1 for no adjustment. (-1)"
    print "\t-e\t--encoder \tEncoder to use. wav, m4a, ogg, mp3. (wav)"
    print "\t-L\t--local   \tUse local data, not solr, with given JSON block (None)"
    print "\t-p\t--print   \tDuring bulk mode, show diagnostic on matches for types fp, fn, fp-a, fp-b (off)"
    print "\t\t--no-shuffle\tdon't randomise the list of input files before running (for testing the exact same files each run) (off)"
    print "\t-m\t--mono    \tMono decoder. (off)"
    print "\t-2\t--22kHz   \tDownsample to 22kHz (off)"
    print "\t-B\t--binary  \tPath to the binary to use for this test (codegen on path)"
    print "\t-t\t--test    \tlist of files to check. pickle of {trid:path, trid2:path2}, or 'none'"
    print "\t-n\t--new     \tnewline separated file of files not in the database, or 'none'"
    print "\t-h\t--help    \tThis help message."
    
def main(argv):
    global _local_bigeval, _new_music_files
    global _new_queries, _old_queries, _total_queries
    
    single = None
    how_many = None
    start = 0
    duration = 30
    bitrate = 128
    volume = -1
    lowpass = -1
    decoder = "mpg123"
    encoder = "wav"
    local = None
    diag = False
    channels = 2
    downsample = False
    decoder = "mpg123"
    testfile = os.path.join(os.path.dirname(__file__), 'bigeval.json')
    newfile = "new_music"
    no_shuffle = False
    
    try:
        opts, args = getopt.getopt(argv, "1:c:s:d:D:b:v:l:L:e:B:t:n:pm2h", 
            ["single=","count=","start=","duration=", "decoder=","bitrate=","volume=","lowpass=",
            "encoder=","print","mono","local=", "test=", "new=","22kHz","help","no-shuffle"])
    except getopt.GetoptError:
        usage()
        sys.exit(2)
    
    for opt,arg in opts:
        if opt in ("-1","--single"):
            single = arg
        if opt in ("-c","--count"):
            how_many = int(arg)
        if opt in ("-s","--start"):
            start = int(arg)
        if opt in ("-d","--duration"):
            duration = int(arg)
        if opt in ("-D","--decoder"):
            decoder = arg
        if opt in ("-b","--bitrate"):
            bitrate = int(arg)
        if opt in ("-v","--volume"):
            volume = int(arg)
        if opt in ("-l","--lowpass"):
            lowpass = int(arg)
        if opt in ("-e","--encoder"):
            encoder = arg
        if opt in ("-L","--local"):
            local = arg
        if opt in ("-p","--print"):
            diag = True
        if opt in ("-m","--mono"):
            channels = 1
        if opt in ("-2","--22kHz"):
            downsample = True
        if opt in ("-B","--binary"):
            if not os.path.exists(arg):
                print "Binary %s not found. Exiting." % arg
                sys.exit(2)
            config.CODEGEN_BINARY_OVERRIDE = arg
        if opt in ("-n","--new"):
            newfile = arg
        if opt in ("-t","--test"):
            testfile = arg
        if opt == "--no-shuffle":
            no_shuffle = True
        if opt in ("-h","--help"):
            usage()
            sys.exit(2)
    
    if (single is None) and (how_many is None):
        print >>sys.stderr, "Run in single mode (-1) or say how many files to test (-c)"
        usage()
        sys.exit(2)
    
    if testfile.lower() == "none" and newfile.lower() == "none" and single is None:
        # If both are none, we can't run
        print >>sys.stderr, "Can't run with no datafiles. Skip --test, --new or add -1"
        sys.exit(2)
    if testfile.lower() == "none":
        _local_bigeval = {}
    else:
        if not os.path.exists(testfile):
            print >>sys.stderr, "Cannot find bigeval.json. did you run fastingest with the -b flag?"
            sys.exit(1)
        _local_bigeval = json.load(open(testfile,'r'))
    if newfile.lower() == "none" or not os.path.exists(newfile):
        _new_music_files = []
    else:
        _new_music_files = open(newfile,'r').read().split('\n')

    _new_queries = float(len(_new_music_files))
    _old_queries = float(len(_local_bigeval.keys()))
    _total_queries = _new_queries + _old_queries
    
    if local is None:
        local = False
    else:
        # ingest
        codes = json.load(open(local,'r'))
        _reversed_bigeval = dict( (_local_bigeval[k], k) for k in _local_bigeval)
        code_dict = {}
        tids = {}
        for c in codes:
            fn = c["metadata"]["filename"]
            tid = _reversed_bigeval.get(fn, None)
            tids[tid] = True
            if tid is not None:
                if c.has_key("code"):
                    if len(c["code"]) > 4:
                        code_dict[tid] = fp.decode_code_string(c["code"])
                        
        fp.ingest(code_dict, local=True)
        lp = {}
        for r in _local_bigeval.keys():
            if tids.has_key(r):
                lp[r] = _local_bigeval[r]
        _local_bigeval = lp
        local = True
        
    if single is not None:
        test_single(single, local=local, start=start, duration = duration, bitrate = bitrate, volume = volume, lowpass_freq = lowpass, encode_to=encoder, downsample_to_22 = downsample, channels = channels)
    else:
        results = test(how_many, diag = diag, local=local, no_shuffle=no_shuffle, start=start, duration = duration, bitrate = bitrate, volume = volume, lowpass_freq = lowpass, encode_to=encoder, downsample_to_22 = downsample, channels = channels)
        prf(results)
        dpwe(results)

if __name__ == '__main__':
    main(sys.argv[1:])


#!/usr/bin/env python
# encoding: utf-8
"""
bigeval.py

Created by <NAME> on 2010-07-02.
Copyright (c) 2010 The Echo Nest Corporation. All rights reserved.
"""

import getopt
import sys
import os
import time
import socket
import subprocess
try:
    import json
except ImportError:
    import simplejson as json
import pyechonest.config as config
import pyechonest.song as song
import random
import math
sys.path.insert(0, "../API")
import fp

config.CODEGEN_BINARY_OVERRIDE = os.path.abspath("../../echoprint-codegen/echoprint-codegen")

_local_bigeval = {}
_new_music_files = []
_new_queries = 0
_old_queries = 0
_total_queries = 0

def decode_to_wav(decoder, file, target, what, start=-1, duration=-1, volume=100, downsample_to_22 = False, channels=2, speed_up=False, slow_down=False):
    """ Given a file, a decoder and bunch of munge parameters, munge the file using the given decoder """

    # We only need to do this once, regardless of decoder
    if start > 0: what.update({"start":start})
    if duration > 0: what.update({"duration":duration})
    if volume > 0: what.update({"volume":volume})
    if downsample_to_22: what.update({"downsample_22":True})
    if channels < 2: what.update({"mono":True})
    # speed_up and slow_down totally both break the FP. This is OK and expected but we should test it anyway.
    if(speed_up): what.update({"skip_every_other_frame":True})
    if(slow_down): what.update({"play_every_frame_2x":True})

    if decoder == 'mpg123':
        cmd = ["mpg123", "-q", "-w", target]
        if start > 0: cmd.extend(["-k", str(int(start*44100 / 1152))])
        if duration > 0: cmd.extend(["-n", str(int(duration*44100 / 1152))])
        if volume > 0: cmd.extend(["-f", str(int( (float(volume)/100.0) * 32768.0 ))])
        if downsample_to_22: cmd.extend(["-2"])
        if channels < 2: cmd.extend(["-0"])
        if speed_up: cmd.extend(["-d", "2"])
        if slow_down: cmd.extend(["-h", "2"])
        cmd.append(file)
    elif decoder =="ffmpeg":
        cmd = ["ffmpeg", "-i", file, "-f", "wav", "-y"]
        if start > 0: cmd.extend(["-ss", str(start)])
        if duration > 0: cmd.extend(["-t", str(duration)])
        #if volume > 0: cmd.extend(["-vol", str(int( (float(volume)/100.0) * 32768.0 ))]
        # -vol is undocumented, but apparently 256 is "normal"
        if downsample_to_22: cmd.extend(["-ar", "22050"])
        if channels < 2: cmd.extend(["-ac", "1"])
        #if speed_up: cmd.extend(["-d", "2"])
        #if slow_down: cmd.extend(["-h", "2"])
        cmd.append(target)
    elif decoder == "mad":
        cmd = ["madplay", "-Q", "-o", "wave:%s" % target]
        if start > 0: cmd.extend(["-s", str(start)])
        if duration > 0: cmd.extend(["-t", str(duration)])
        #if volume > 0: cmd.extend(["-f", str(int( (float(volume)/100.0) * 32768.0 ))]
        # --attenuate or --amplify takes in dB -> need to convert % to db
        if downsample_to_22: cmd.extend("--downsample")
        if channels < 2: cmd.extend(["-m"])
        #if speed_up: cmd.extend(["-d", "2"])
        #if slow_down: cmd.extend(["-h", "2"])
        cmd.append(file)
    elif decoder == "sox":
        cmd = ["sox", file, target]
        if start < 0: start = 0
        cmd.extend(["trim", str(start)])
        if duration > 0: cmd.extend([str(duration)])
        #if volume > 0: cmd.extend()
        if downsample_to_22: cmd.extend(["rate", "22050"])
        if channels < 2: cmd.extend(["channels", "1"])
        if speed_up: cmd.extend(["speed", "2.0"])
        if slow_down: cmd.extend(["speed", "0.5"])
    else:
        return (what, None)
    """
    # These will decode an mp3 to wav, but can't handle start/duration parameters
    elif decoder == "lame":
        cmd = ["lame", "--decode", file, target]
    elif decoder == "afconvert":
        cmd = ["afconvert", "-f", "WAVE", "-d", "LEI16", file, target]
    """
    subprocess.Popen(cmd, stderr=subprocess.PIPE).communicate()
    return (what, target)

def munge(file, start=-1, duration=-1, bitrate=128, volume=-1, downsample_to_22 = False, speed_up = False, slow_down = False, lowpass_freq = -1, encode_to="mp3", decoder="mpg123", channels=2):
    """
        duration: seconds of source file
        start: seconds to start reading
        volume 0-100, percentage of original
        bitrate: 8, 16, 32, 64, 96, 128, 160, 192, 256, 320
        downsample_to_22: True or False
        speed_up: True or False
        slow_down: True or False
        lowpass_freq: -1 (don't), 22050, 16000, 12000, 8000, 4000
        encode_to: mp3 (uses LAME), m4a (uses ffmpeg), wav, ogg (uses ffmpeg) (ogg does not work yet)
        channels: 1 or 2
    """
    
    # Get a tempfile to munge to
    me = "/tmp/temp_"+str(random.randint(1,32768))+".wav"
    what = {"decoder": decoder}

    (what, me) = decode_to_wav(decoder, file, me, what, start, duration, volume, downsample_to_22, channels, speed_up, slow_down)

    if not os.path.exists(me):
        print >> sys.stderr, "munge result not there"
        return (None, what)
        
    file_size = os.path.getsize(me)
    if(file_size<100000):
        print >> sys.stderr, "munge too small"
        os.remove(me)
        return (None, what)

    roughly_in_seconds = file_size / 176000
    what.update({"actual_file_length":roughly_in_seconds})

    if encode_to == "wav":
        what.update({"encoder":"none","encode_to":"wav"})
        return (me, what)

    if encode_to == "mp3":
        what.update({"encoder":"lame","encode_to":"mp3"})
        cmd = "lame --silent -cbr -b " + str(bitrate) + " "
        if(lowpass_freq > 0):
            what.update({"lowpass":lowpass_freq})
            cmd = cmd + " --lowpass " + str(lowpass_freq) + " "
        what.update({"bitrate":bitrate})
        cmd = cmd + me + " " + me + ".mp3"
    
    if encode_to == "m4a":
        what.update({"encoder":"ffmpeg","encode_to":"m4a"})
        cmd = "ffmpeg -i " + me + " -ab " + str(bitrate) + "k " + me + ".m4a 2>/dev/null"

    # NB ogg does not work on my copy of ffmpeg...
    if encode_to == "ogg":
        what.update({"encoder":"ffmpeg","encode_to":"ogg"})
        cmd = "ffmpeg -i " + me + " -ab " + str(bitrate) + "k " + me + ".ogg 2>/dev/null"

    os.system(cmd)
    try:
        os.remove(me)
    except OSError:
        return (None, what)
    return (me+"."+encode_to, what)

def prf(numbers_dict):
    # compute precision, recall, F, etc
    precision = recall = f = true_negative_rate = accuracy = 0
    tp = float(numbers_dict["tp"])
    tn = float(numbers_dict["tn"])
    fp = float(numbers_dict["fp-a"]) + float(numbers_dict["fp-b"])
    fn = float(numbers_dict["fn"])
    if tp or fp:
        precision = tp / (tp + fp)
    if fn or tp:
        recall = tp / (tp + fn)
    if precision or recall:
        f = 2.0 * (precision * recall)/(precision + recall)
    if tn or fp:
        true_negative_rate = tn / (tn + fp)
    if tp or tn or fp or fn:
        accuracy = (tp+tn) / (tp + tn + fp + fn)
    print "P %2.4f R %2.4f F %2.4f TNR %2.4f Acc %2.4f %s" % (precision, recall, f, true_negative_rate, accuracy, str(numbers_dict))
    return {"precision":precision, "recall":recall, "f":f, "true_negative_rate":true_negative_rate, "accuracy":accuracy}

def dpwe(numbers_dict):
    # compute dan's measures.. probability of error, false accept rate, correct accept rate, false reject rate
    car = far = frr = pr = 0
    r1 = float(numbers_dict["tp"])
    r2 = float(numbers_dict["fp-a"])
    r3 = float(numbers_dict["fn"])
    r4 = float(numbers_dict["fp-b"])
    r5 = float(numbers_dict["tn"])
    if r1 or r2 or r3:
        car = r1 / (r1 + r2 + r3)
    if r4 or r5:
        far = r4 / (r4 + r5)
    if r1 or r2 or r3:
        frr = (r2 + r3) / (r1 + r2 + r3)
    # probability of error
    pr = ((_old_queries / _total_queries) * frr) + ((_new_queries / _total_queries) * far)    
    print "PR %2.4f CAR %2.4f FAR %2.4f FRR %2.4f %s" % (pr, car, far, frr, str(numbers_dict))
    stats = {}
    stats.update(numbers_dict)    
    dpwe_nums = {"pr":pr, "car": car, "far":far, "frr":frr}
    stats.update(dpwe_nums)
    return dpwe_nums

def test_file(filename, local = False, expect_match=True, original_TRID=None, remove_file = True):
    """
        Test a single file. This will return a code like tn, tp, fp, err, etc
    """
    matching_TRID = None
    if filename is None:
        return "err-munge" # most likely a munge error (file too short, unreadable, etc)
    try:
        # don't pass start and duration to codegen, assume the munging takes care of codelength
        if not local:
            query_obj = song.util.codegen(filename, start=-1, duration=-1)
            s = fp.best_match_for_query(query_obj[0]["code"])
            if s.TRID is not None:
                matching_TRID = s.TRID
        else:
            query_obj = song.util.codegen(filename, start=-1, duration=-1)
            s = fp.best_match_for_query(query_obj[0]["code"], local=local)
            if s.TRID is not None:
                matching_TRID = s.TRID

    except IOError:
        print "TIMEOUT from API server"
        return "err-api"
    except TypeError: # codegen returned none
        return "err-codegen"
    if remove_file:
        if os.path.exists(filename):
            os.remove(filename)
        else:
            return "err-munge"

    # if is not None there was a response
    if s is not None:
        # there was a match
        if len(s) > 0:
            # if we're expecting a match, check that it's the right one
            if expect_match:
                if original_TRID == matching_TRID:
                    # we expected a match, it's the right one. TP
                    return "tp"
                else:
                    # we expected a match but it's the wrong one. FP-a
                    return "fp-a"
            else:
                # we did not expect a match. FP-b
                return "fp-b"
        else:
            # there was no match from the API
            if expect_match:
                # we expected a match. FN
                return "fn"
            else:
                # we did not expect a match. TN
                return "tn"
    else:
        # s is None, that means API error-- almost definitely codegen returned nothing.
        return "err-codegen"

def test_single(filename, local=False, **munge_kwargs):
    """
        Perform a test on a single file. Prints more diagnostic information than usual.
    """
    (new_file, what) = munge(filename, **munge_kwargs)
    query_obj = song.util.codegen(new_file, start=-1, duration=-1)
    s = fp.best_match_for_query(query_obj[0]["code"],local=local)
    if s.TRID is not None:
        if local:
            metad = _local_bigeval[s.TRID]
        else:
            metad = fp.metadata_for_track_id(s.TRID)
            metad["title"] = metad["track"]
        song_metadata = {"artist": metad.get("artist", ""), "release": metad.get("release", ""), "title": metad.get("title", "")}
        print str(song_metadata)
    else:
        print "No match"
    
    decoded = fp.decode_code_string(query_obj[0]["code"])
    print str(len(decoded.split(" "))/2) + " codes in original"
    response = fp.query_fp(decoded, local=local, rows=15)
    if response is not None:
        print "From FP flat:"
        tracks = {}
        scores = {}
        for r in response.results:
            trid = r["track_id"].split("-")[0]
            if local:
                metad = _local_bigeval[trid]
            else:
                metad = fp.metadata_for_track_id(trid)
                metad["title"] = metad["track"]
            m = {"artist": metad.get("artist", ""), "release": metad.get("release", ""), "title": metad.get("title", "")}
            if m is not None:
                actual_match = fp.actual_matches(decoded, fp.fp_code_for_track_id(r["track_id"], local=local))
                tracks[r["track_id"]] = (m, r["score"], actual_match)
                scores[r["track_id"]] = actual_match
            else:
                print "problem getting metadata for " + r["track_id"]
        sorted_scores = sorted(scores.iteritems(), key=lambda (k,v): (v,k), reverse=True)
        for (trackid, score) in sorted_scores:
            (m, score, actual_match) = tracks[trackid]
            print trackid + " (" + str(int(score)) + ", " + str(actual_match) +") - " + m["artist"] + " - " + m["title"]
    else:
        print "response from fp flat was None -- decoded code was " + str(decoded)
    os.remove(new_file)

def test(how_many, diag=False, local=False, no_shuffle=False, **munge_kwargs):
    """
        Perform a test. Takes both new files and old files, munges them, tests the FP with them, computes various measures.
        how_many: how many files to process
        munge_kwargs: you can pass any munge parameter, like duration=30 or volume=50
    """
    results = {"fp-a":0, "fp-b":0, "fn":0, "tp":0, "tn":0, "err-codegen":0, "err-munge":0, "err-api":0, "err-data":0, "total":0}
    
    docs_to_test = _local_bigeval.keys() + _new_music_files
    if not no_shuffle:
        random.shuffle(docs_to_test)

    for x in docs_to_test:
        if results["total"] == how_many:
            return results

        if x.startswith("TR"): # this is a existing TRID
            original_TRID = x
            filename = _local_bigeval[x]["filename"]
            (new_file, what) = munge(filename, **munge_kwargs)
            result = test_file(new_file, expect_match = True, original_TRID = x, local=local)
                
        else: # this is a new file
            filename = x
            original_TRID = None
            (new_file, what) = munge(filename, **munge_kwargs)
            result = test_file(new_file, expect_match = False, local=local)
        
        if result is not "tp" and result is not "tn":
            print "BAD ### " + filename + " ### " + result + " ### " + str(original_TRID) + " ### " + str(what)
            if diag and not result.startswith("err"):
                test_single(filename, local=local, **munge_kwargs)
            
        results[result] += 1
        results["total"] += 1

        if results["total"] % 10 == 0:
            dpwe(results)

    return results
    
def usage():
    print "FP bigeval"
    print "\t-1\t--single  \tSingle mode, given a filename show an answer"
    print "\t-c\t--count   \tHow many files to process (required if not --single)"
    print "\t-s\t--start   \tIn seconds, when to start decoding (0)"
    print "\t-d\t--duration\tIn seconds, how long to decode, -1 is unchanged (30)"
    print "\t-D\t--decoder \tWhat decoder to use to make pcm ([mpg123|ffmpeg|sox|mad])"
    print "\t-b\t--bitrate \tIn kbps, encoded bitrate. only for mp3, m4a, ogg. (128)"
    print "\t-v\t--volume  \tIn %, volume of original. -1 for no adjustment. (-1)"
    print "\t-l\t--lowpass \tIn Hz, lowpass filter. -1 for no adjustment. (-1)"
    print "\t-e\t--encoder \tEncoder to use. wav, m4a, ogg, mp3. (wav)"
    print "\t-L\t--local   \tUse local data, not solr, with given JSON block (None)"
    print "\t-p\t--print   \tDuring bulk mode, show diagnostic on matches for types fp, fn, fp-a, fp-b (off)"
    print "\t\t--no-shuffle\tdon't randomise the list of input files before running (for testing the exact same files each run) (off)"
    print "\t-m\t--mono    \tMono decoder. (off)"
    print "\t-2\t--22kHz   \tDownsample to 22kHz (off)"
    print "\t-B\t--binary  \tPath to the binary to use for this test (codegen on path)"
    print "\t-t\t--test    \tlist of files to check. pickle of {trid:path, trid2:path2}, or 'none'"
    print "\t-n\t--new     \tnewline separated file of files not in the database, or 'none'"
    print "\t-h\t--help    \tThis help message."
    
def main(argv):
    global _local_bigeval, _new_music_files
    global _new_queries, _old_queries, _total_queries
    
    single = None
    how_many = None
    start = 0
    duration = 30
    bitrate = 128
    volume = -1
    lowpass = -1
    decoder = "mpg123"
    encoder = "wav"
    local = None
    diag = False
    channels = 2
    downsample = False
    decoder = "mpg123"
    testfile = os.path.join(os.path.dirname(__file__), 'bigeval.json')
    newfile = "new_music"
    no_shuffle = False
    
    try:
        opts, args = getopt.getopt(argv, "1:c:s:d:D:b:v:l:L:e:B:t:n:pm2h", 
            ["single=","count=","start=","duration=", "decoder=","bitrate=","volume=","lowpass=",
            "encoder=","print","mono","local=", "test=", "new=","22kHz","help","no-shuffle"])
    except getopt.GetoptError:
        usage()
        sys.exit(2)
    
    for opt,arg in opts:
        if opt in ("-1","--single"):
            single = arg
        if opt in ("-c","--count"):
            how_many = int(arg)
        if opt in ("-s","--start"):
            start = int(arg)
        if opt in ("-d","--duration"):
            duration = int(arg)
        if opt in ("-D","--decoder"):
            decoder = arg
        if opt in ("-b","--bitrate"):
            bitrate = int(arg)
        if opt in ("-v","--volume"):
            volume = int(arg)
        if opt in ("-l","--lowpass"):
            lowpass = int(arg)
        if opt in ("-e","--encoder"):
            encoder = arg
        if opt in ("-L","--local"):
            local = arg
        if opt in ("-p","--print"):
            diag = True
        if opt in ("-m","--mono"):
            channels = 1
        if opt in ("-2","--22kHz"):
            downsample = True
        if opt in ("-B","--binary"):
            if not os.path.exists(arg):
                print "Binary %s not found. Exiting." % arg
                sys.exit(2)
            config.CODEGEN_BINARY_OVERRIDE = arg
        if opt in ("-n","--new"):
            newfile = arg
        if opt in ("-t","--test"):
            testfile = arg
        if opt == "--no-shuffle":
            no_shuffle = True
        if opt in ("-h","--help"):
            usage()
            sys.exit(2)
    
    if (single is None) and (how_many is None):
        print >>sys.stderr, "Run in single mode (-1) or say how many files to test (-c)"
        usage()
        sys.exit(2)
    
    if testfile.lower() == "none" and newfile.lower() == "none" and single is None:
        # If both are none, we can't run
        print >>sys.stderr, "Can't run with no datafiles. Skip --test, --new or add -1"
        sys.exit(2)
    if testfile.lower() == "none":
        _local_bigeval = {}
    else:
        if not os.path.exists(testfile):
            print >>sys.stderr, "Cannot find bigeval.json. did you run fastingest with the -b flag?"
            sys.exit(1)
        _local_bigeval = json.load(open(testfile,'r'))
    if newfile.lower() == "none" or not os.path.exists(newfile):
        _new_music_files = []
    else:
        _new_music_files = open(newfile,'r').read().split('\n')

    _new_queries = float(len(_new_music_files))
    _old_queries = float(len(_local_bigeval.keys()))
    _total_queries = _new_queries + _old_queries
    
    if local is None:
        local = False
    else:
        # ingest
        codes = json.load(open(local,'r'))
        _reversed_bigeval = dict( (_local_bigeval[k], k) for k in _local_bigeval)
        code_dict = {}
        tids = {}
        for c in codes:
            fn = c["metadata"]["filename"]
            tid = _reversed_bigeval.get(fn, None)
            tids[tid] = True
            if tid is not None:
                if c.has_key("code"):
                    if len(c["code"]) > 4:
                        code_dict[tid] = fp.decode_code_string(c["code"])
                        
        fp.ingest(code_dict, local=True)
        lp = {}
        for r in _local_bigeval.keys():
            if tids.has_key(r):
                lp[r] = _local_bigeval[r]
        _local_bigeval = lp
        local = True
        
    if single is not None:
        test_single(single, local=local, start=start, duration = duration, bitrate = bitrate, volume = volume, lowpass_freq = lowpass, encode_to=encoder, downsample_to_22 = downsample, channels = channels)
    else:
        results = test(how_many, diag = diag, local=local, no_shuffle=no_shuffle, start=start, duration = duration, bitrate = bitrate, volume = volume, lowpass_freq = lowpass, encode_to=encoder, downsample_to_22 = downsample, channels = channels)
        prf(results)
        dpwe(results)

if __name__ == '__main__':
    main(sys.argv[1:])


#!/usr/bin/env python # encoding: utf-8 bigeval.py  Created by <NAME> on 2010-07-02. Copyright (c) 2010 The Echo Nest Corporation. All rights reserved. Given a file, a decoder and bunch of munge parameters, munge the file using the given decoder # We only need to do this once, regardless of decoder # speed_up and slow_down totally both break the FP. This is OK and expected but we should test it anyway. #if volume > 0: cmd.extend(["-vol", str(int( (float(volume)/100.0) * 32768.0 ))] # -vol is undocumented, but apparently 256 is "normal" #if speed_up: cmd.extend(["-d", "2"]) #if slow_down: cmd.extend(["-h", "2"]) #if volume > 0: cmd.extend(["-f", str(int( (float(volume)/100.0) * 32768.0 ))] # --attenuate or --amplify takes in dB -> need to convert % to db #if speed_up: cmd.extend(["-d", "2"]) #if slow_down: cmd.extend(["-h", "2"]) #if volume > 0: cmd.extend() # These will decode an mp3 to wav, but can't handle start/duration parameters     elif decoder == "lame":         cmd = ["lame", "--decode", file, target]     elif decoder == "afconvert":         cmd = ["afconvert", "-f", "WAVE", "-d", "LEI16", file, target] duration: seconds of source file         start: seconds to start reading         volume 0-100, percentage of original         bitrate: 8, 16, 32, 64, 96, 128, 160, 192, 256, 320         downsample_to_22: True or False         speed_up: True or False         slow_down: True or False         lowpass_freq: -1 (don't), 22050, 16000, 12000, 8000, 4000         encode_to: mp3 (uses LAME), m4a (uses ffmpeg), wav, ogg (uses ffmpeg) (ogg does not work yet)         channels: 1 or 2 # Get a tempfile to munge to # NB ogg does not work on my copy of ffmpeg... # compute precision, recall, F, etc # compute dan's measures.. probability of error, false accept rate, correct accept rate, false reject rate # probability of error Test a single file. This will return a code like tn, tp, fp, err, etc # most likely a munge error (file too short, unreadable, etc) # don't pass start and duration to codegen, assume the munging takes care of codelength # codegen returned none # if is not None there was a response # there was a match # if we're expecting a match, check that it's the right one # we expected a match, it's the right one. TP # we expected a match but it's the wrong one. FP-a # we did not expect a match. FP-b # there was no match from the API # we expected a match. FN # we did not expect a match. TN # s is None, that means API error-- almost definitely codegen returned nothing. Perform a test on a single file. Prints more diagnostic information than usual. Perform a test. Takes both new files and old files, munges them, tests the FP with them, computes various measures.         how_many: how many files to process         munge_kwargs: you can pass any munge parameter, like duration=30 or volume=50 # this is a existing TRID # this is a new file ### " + filename + " ### " + result + " ### " + str(original_TRID) + " ### " + str(what) # If both are none, we can't run # ingest