Play a Song for Me

23

3

Challenge

Given guitar tablature you must output the song represented by the tab. This may be to the speakers of your computer or to an audio file (.wav, .mp3, .midi, .aiff, etc.). There will also be a second input for timing.

The tabs may be input via a file or straight to STDIN. The tab will be in ASCII form.

Spec

All tabs are for 6 six string guitars with standard E tuning: E2 (82.41 Hz), A2 (110.00 Hz), D3 (146.83 Hz), G3 (196.00 Hz), B3 (246.94 Hz), E4 (329.63 Hz).

The only techniques (besides normal picking) you have to cater for are:

  • Bending (this will always be a half tone bend)
  • Hammering on
  • Pulling off
  • Sliding up/down

Since you cannot synthesise the sound of a muted string, treat x as a -.

When bending, output the full transition from unbent to string to bent to unbent again.

The second input will be the time that each symbol on the tab represents in seconds. For example:

For input:

e|---
B|---
G|---
D|---
A|---
E|---

With timing 0.5, because there are 3 columns of symbols (but no notes), the outputted audio file is (3*0.5=1.5) 1.5 seconds of silence.

Example tabs

1 - The Weight (Jack White, Jimmy Page + The Edge edition)

e|----3-----3---3----2---------3--------------------|
B|----3-----3---3----3--1-1----3--------------------|
G|----0-----0---0----2--0-0----0--------------------|
D|----0-----0---2-------2-2----0--------------------|          
A|----2-----0---2-------3-3----2--------------------|     
E|----3-----2---x----2--x-x----3--------------------|   

2 - Smells Like Teen Spirit

e|--------------|---------------|-------------|-------------|
B|--------------|---------------|-------------|-------------|
G|-----8h10-----|-8-8b----6--5--|-6--5--------|-------------|
D|-10--------6--|---------------|-------8-6-8-|-8b----6--5--|
A|--------------|---------------|-------------|-------------|
E|--------------|---------------|-------------|-------------|

3 - Star Spangled Banner

e|---0-------2-5---9-7-5-----------9-7-5-4-2-4-5------|
B|-----2---2-------------2-4-5---5---------------5-2--|
G|-------2-------------------------------------------2|
D|----------------------------------------------------|
A|----------------------------------------------------|
E|----------------------------------------------------|

Beta Decay

Posted 2015-06-29T23:39:10.650

Reputation: 21 478

3I added a few more decimal places to your frequencies. Given that one semitone = 1 fret is a ratio of 1.059463:1 (i.e. a difference of about 6%) tuning to the nearest 1Hz is not precise enough to get a good in-tune sound. Of course being a popularity contest, poor tuning may be admissible but it won't win. – Level River St – 2015-06-30T00:06:27.280

Very creative contest! After I looked at the link to ASCII form, I could understand example 2 (since I've heard the song), but because I don't know guitar, I think the challenge has a high learning curve. I also have little experience with audio manipulation other than basic usage of Audacity. – mbomb007 – 2015-06-30T17:32:37.647

Does MIDI count as an 'audio file'? – orlp – 2015-07-01T08:48:19.330

@orlp Yes, it does – Beta Decay – 2015-07-01T08:48:41.613

I clearly failed. I should have googled guitar tablature before I made my program. I thought that the numbers meant how long the note lasted.

– Grant Davis – 2015-07-03T03:10:21.467

@GrantDavis I tried that for Smells Like Teen Spirit but all I could hear was clicks every time the cursor moved... – Beta Decay – 2015-07-03T08:48:05.660

@BetaDecay Strange. It works on my Computer. Either way, I will be updating the program, now that I have learned more about guitar tabs. – Grant Davis – 2015-07-03T20:18:54.953

@GrantDavis Hm maybe it was because I was using Firefox on Ubuntu... Anyway, do you think I should add something about the numbers on the tab itself into the question? – Beta Decay – 2015-07-03T20:29:13.630

1Well for future reference: v*(2^(f/12))=x; v=frequency of string; f=Fret (the number on the tab); x=frequency played; Tabs also do not tell you the length of a note; your program need to be smart. – Grant Davis – 2015-07-05T00:27:56.443

@GrantDavis I expect you not to consider the length of the note, that's just based upon the player's judgement as to whether it sounds right or not – Beta Decay – 2015-07-05T00:29:37.813

Answers

7

MATLAB

This is sort of unfinished. I used a quick and dirty method to make the audio as easily as possible. The method I used made it difficult to implement the bending/hammering (I also had never heard those words in this context before).

Having said all of that, this script will read in a text file called "inputs.txt" containing the ascii-tab as required and play the song.

%timing
t=0.25;  %of course, this line could be 't=input('timing: ');
        %if you make t a wonky value such that t*8192 is not an integer, some
        %stuff will fail
%frequencies and extra variables to allow some laziness later
e = 329.63;eN = 1;
B = 246.94;BN = 2;
G = 196.00;GN = 3;
D = 146.83;DN = 4;
A = 110.00;AN = 5;
E = 82.41;EN = 6;
%this will store the song in a more computer friendly way
song = zeros(1,6);
%function to get frequency from v=frequency and f=fret
w=@(v,f)v*(2^(f/12));
%get input and start the big loop
file = fopen('input.txt');
line = fgetl(file);
while ischar(line)
    %the first character of the line will give us the the line frequency
    lfreqv = eval(line(1));         %frequency
    lfreqN = eval([line(1),'N']);   %horizontal index of frequency
    %start the small loop over each line
    for k = 3:(numel(line))-1
        if (strcmp(line(k),'-'))||(strcmp(line(k),'|'))||(strcmp(line(k),'h'))||(strcmp(line(k),'b'))
            song(k-2,lfreqN) = 0;
        else
            song(k-2,lfreqN) = w(lfreqv,double(line(k)));
        end
    end
    line = fgetl(file);
end
fclose(file);
%this will hold the song
tune = [];
vols = zeros(1,6);
playf = zeros(1,6);
for songIndex = 1:size(song,1)
    ctune = [];
    for k=1:6
        if song(songIndex,k) == 0
            vols(k) = 2*vols(k)/4;
        else
            vols(k) = 1;
            playf(k) = song(songIndex,k);
        end
        ctune(k,1:t*8192)=vols(k)*sin(0.5*pi*playf(k)*(1:(t*8192))/8192);
    end
    tune = [tune ctune];
end
soundsc(sum(tune));

Here is a link to the sound of the first test input.

Here is a link to the sound of the third test input. (Star Spangled Banner or Ice Cream Truck?)

The second test input sounded pretty bad to me, but that may be because it uses a lot of bs and hs which the script ignores.

As you can hear, the output is not quite the same quality as the original. It sort of sounds like there's a metronome playing in the background. I think these tunes have character.

sudo rm -rf slash

Posted 2015-06-29T23:39:10.650

Reputation: 1 076

Wow, that sounds like a music box... Really nice! – Beta Decay – 2015-07-03T08:50:13.680

5

Python 3

I had to try this one.

This converts a tab to a midi file as played by a piano. I have no idea how to do a string bending on a piano, so it can't do that, but hammer-on's and pull-off's are straightforward.

I generated the test files like so: $ python3 tab.py The-weight.txt 0.14 where 0.14 is the length of a single note in seconds.

from midiutil.MidiFile3 import MIDIFile
import sys

# Read the relevant lines of the file
lines = []
if len(sys.argv) > 1:
    filename = sys.argv[1]
    try:
        beats_per_minute = 60 / float(sys.argv[2])
    except:
        beats_per_minute = 756
else:
    filename = 'mattys-tune.txt'
    beats_per_minute = 756
with open(filename) as f:
    for line in f:
        if len(line) > 3 and (line[1] == '|' or line[2] == '|'):
            line = line.replace('\n', '')
            lines.append(line)
assert len(lines) % 6 == 0

# Initialize the MIDIFile object (with 1 track)
time = 0
duration = 10
volume = 100
song = MIDIFile(1)
song.addTrackName(0, time, "pianized_guitar_tab.")
song.addTempo(0, time, beats_per_minute)

# The root-pitches of the guitar
guitar = list(reversed([52, 57, 62, 67, 71, 76])) # Assume EADGBe tuning
def add_note(string, fret):
    song.addNote(0, string, guitar[string] + fret, time, duration, volume)

# Process the entire tab
for current in range(0, len(lines), 6):  # The current base string
    for i in range(len(lines[current])): # The position in the current string
        time += 1
        for s in range(6):               # The number of the string
            c = lines[current + s][i]
            try: next_char = lines[current + s][i + 1]
            except: next_char = ''
            if c in '-x\\/bhp':
                # Ignore these characters for now
                continue
            elif c.isdigit():
                # Special case for fret 10 and higher
                if next_char.isdigit():
                    c += next_char
                    lines[current + s] = lines[current + s][:i+1] + '-' + lines[current + s][i+2:]
                # It's a note, play it!
                add_note(s, int(c))
            else:
                # Awww
                time -= 1
                break

# And write it to disk.
def save():
    binfile = open('song.mid', 'wb')
    song.writeFile(binfile)
    binfile.close()
    print('Done')
try:
    save()
except:
    print('Error writing to song.mid, try again.')
    input()
    try:
        save()
    except:
        print('Failed!')

The code is on github too, https://github.com/Mattias1/ascii-tab, where I also uploaded the result of the examples provided by the OP. I also tried it on some of my own tabs. It's quite weird to hear a piano play it, but it's not bad.

Examples:

I added some direct links, but not sure how long they stay, so I'll keep the old download links as well.

  1. The weight, or play
  2. Smells like teen spirit, or play
  3. Star sprangled banner, or play
  4. Matty's tune, or play
  5. dm tune, or play

And the tab from Matty's tune (my favourite) below:

    Am/C        Am            F          G             Am/C        Am
e |------------------------|----------------0-------|------------------------|
B |-1--------1--1--------1-|-1--------1--3-----3----|-1--------1--1--------1-|
G |-2-----2-----2-----2----|-2-----2--------------0-|-2-----2-----2-----2----|
D |----2-----------2-------|----2-------------------|----2-----------2-------|
A |-3-----2-----0----------|-------0--------0--2----|-3-----------0----------|
E |-------------------3----|-1-----------3----------|------------------------|

    F        G               Am/C        Am           F           G
e |------------------------|------------------------|----------------0-------|
B |-1--------3-------------|-1--------1--1--------1-|-1--------1--3-----3----|
G |----------4-------------|-2-----2-----2-----2----|-2-----2--------------0-|
D |-------3--5-------------|----2-----------2-------|----2-------------------|
A |----3-----5--------0--2-|-3-----2-----0----------|-------0--------0--2----|
E |-1--------3-----3-------|-------------------3----|-1-----------3----------|

    Am/C        Am           F        G
e |------------------------|------------------------|
B |-1--------1--1--------1-|-1----------3-----------|
G |-2-----2-----2-----2----|------------4-----------|
D |----2-----------2-------|-------3---5------------|
A |-3-----------0----------|----3------5------------|
E |------------------------|-1--------3-------------|

Matty

Posted 2015-06-29T23:39:10.650

Reputation: 471

1Woah, 756 BPM?! I hope that's not the final beat... – Beta Decay – 2015-07-04T19:41:11.057

Haha, well, I cheat a little bit. 2/3 of those 'beats' are in fact dashes. – Matty – 2015-07-04T22:44:18.283

Woah, Matty's tune sounds pretty cool. What's it like on a guitar? – Beta Decay – 2015-07-04T22:47:42.953

1

Thanks @BetaDecay, it's a tune I once made, (the baseline) inspired by Tommy Emmanuel's blue moon (https://www.youtube.com/watch?v=v0IY3Ax2PkY). But it doesn't sound half as good as how he does it.

– Matty – 2015-07-05T10:43:56.017

4

Java Script

Note: Uses Web Development Audio Kit; This is way out of IE's League; Tested in Google Chrome

You can put the tabs in the textarea. IE you could put Matty's tune from Matty's post in the textarea (with the letters over the notes) and it will still parse correctly.

Click to Run Program

JavaScript:

context = new AudioContext;
gainNode = context.createGain();
gainNode.connect(context.destination);

gain= 2;

function getValue(i) {
    return document.getElementById(i).value;
}

function addValue(i, d) {
    document.getElementById(i).value += d;
}

function setValue(i, d) {
    document.getElementById(i).value = d;
}

document.getElementById("tada").onclick = updateLines;

function updateLines(){
    var e=getValue("ta").replace(/[^-0-9\n]/g,'').replace("\n\n","\n").split("\n");
    for(var l=0;l<e.length;l+=6){
        try{
        addValue("littleE",e[l]);
        addValue("B",e[l+1]);
        addValue("G",e[l+2]);
        addValue("D",e[l+3]);
        addValue("A",e[l+4]);
        addValue("E",e[l+5]);
        }catch(err){}
    }
    updateDash();
}

document.getElementById("littleE").oninput = updateDash;
document.getElementById("B").oninput = updateDash;
document.getElementById("G").oninput = updateDash;
document.getElementById("D").oninput = updateDash;
document.getElementById("A").oninput = updateDash;
document.getElementById("E").oninput = updateDash;


function updateDash() {
    max = 10;
    findDashMax("littleE");
    findDashMax("B");
    findDashMax("G");
    findDashMax("D");
    findDashMax("A");
    findDashMax("E");
    applyMax();
    i = "littleE";
    dash = new Array();
    for (var l = 0; l < getValue(i).length; l++) {
        if (getValue(i).charCodeAt(l) == 45) {
            dash[l] = true;
        } else {
            dash[l] = false;
        }
    }
    /*applyDash("B");
    applyDash("G");
    applyDash("D");
    applyDash("A");
    applyDash("E");*/
}

function findDashMax(i) {
    if (getValue(i).length > max) {
        max = getValue(i).length;
    }
}

function applyMax() {
    if (max < 50) {
        document.getElementById("stepe").size = 50;
        document.getElementById("littleE").size = 50;
        document.getElementById("B").size = 50;
        document.getElementById("G").size = 50;
        document.getElementById("D").size = 50;
        document.getElementById("A").size = 50;
        document.getElementById("E").size = 50;
    } else {
        document.getElementById("stepe").size = max + 1;
        document.getElementById("littleE").size = max + 1;
        document.getElementById("B").size = max + 1;
        document.getElementById("G").size = max + 1;
        document.getElementById("D").size = max + 1;
        document.getElementById("A").size = max + 1;
        document.getElementById("E").size = max + 1;
    }
}

function applyDash(i) {
    var old = getValue(i);
    setValue(i, "");
    for (var l = 0; l < old.length || dash[l] == true; l++) {
        if (dash[l] == true) {
            addValue(i, "-");
        } else {
            if (old.charCodeAt(l) != 45) {
                addValue(i, old.charAt(l));
            }
        }
    }
}
document.getElementById("next").onclick = begin;

function addDash(i) {
    while (getValue(i).length < max) {
        addValue(i, "-");
    }
}

function begin() {
    setValue("littleE",getValue("littleE").replace(/[^-0-9]/g,''));
    setValue("B",getValue("B").replace(/[^-0-9]/g,''));
    setValue("G",getValue("G").replace(/[^-0-9]/g,''));
    setValue("D",getValue("D").replace(/[^-0-9]/g,''));
    setValue("A",getValue("A").replace(/[^-0-9]/g,''));
    setValue("E",getValue("E").replace(/[^-0-9]/g,''));
    addDash("littleE");
    addDash("B");
    addDash("G");
    addDash("D");
    addDash("A");
    addDash("E");
    setValue("next", "Stop");
    //playing = true;
    findLength();
    document.getElementById("next").onclick = function () {
        clearInterval(playingID);
        oscillator["littleE"].stop(0);
        oscillator["B"].stop(0);
        oscillator["G"].stop(0);
        oscillator["D"].stop(0);
        oscillator["A"].stop(0);
        oscillator["E"].stop(0);
        setValue("next", "Play");
        document.getElementById("next").onclick = begin;
    }
    step = -1;
    playingID = setInterval(function () {
        step++;
        setValue("stepe", "");
        for (var l = 0; l < step; l++) {
            addValue("stepe", " ");
        }
        addValue("stepe", "V");
        if (lg[step]) {
            oscillator["littleE"].stop(0);
            oscillator["B"].stop(0);
            oscillator["G"].stop(0);
            oscillator["D"].stop(0);
            oscillator["A"].stop(0);
            oscillator["E"].stop(0);
        }
        qw=0
        doSound("littleE");
        doSound("B");
        doSound("G");
        doSound("D");
        doSound("A");
        doSound("E");

    }, getValue("s") * 1000);
}

function doSound(i) {
    switch (getValue(i).charAt(step)) {
        case ("-"):
        case ("x"):
        case (""):
        case (" "):
            break;
        default:
            qw++;
            makeSound(fretToHz(getHz(i), getValue(i).charAt(step)), i);

    }
    checkTop();
}

function checkTop(){
    switch(qw){
        case 0:
            break;
        case 1:
            gain=2;
            break;
        case 2:
            gain=1;
            break;
        case 3:
            gain=.5;
            break;
        default:
            gain=.3;
            break;
    }
}

function getHz(i) {
    switch (i) {
        case "littleE":
            return 329.63;
        case "B":
            return 246.94;
        case "G":
            return 196;
        case "D":
            return 146.83;
        case "A":
            return 110;
        case "E":
            return 82.41;
    }
}

function fretToHz(v, f) {
    return v * (Math.pow(2, (f / 12)));
}

/*function getTime() {
    var u = 1;
    while (lg[step + u] == false) {
        u++;
    }
    return u;
}*/

function findLength() {
    lg = new Array();
    for (var h = 0; h < getValue("littleE").length; h++) {
        lg[h] = false;
        fl(h, "littleE");
        fl(h, "B");
        fl(h, "G");
        fl(h, "D");
        fl(h, "A");
        fl(h, "E");
    }
    console.table(lg);
}

function fl(h, i) {
    var l = getValue(i).charAt(h);
    switch (l) {
        case "-":
        case "|":
            break;
        default:
            lg[h] = true;
    }
}

oscillator = new Array();

function makeSound(hz, i) {
    console.log("playing " + hz + " Hz" + i);
    oscillator[i] = context.createOscillator();
    oscillator[i].connect(gainNode);
    oscillator[i].frequency.value = hz;
    oscillator[i].start(0);
}

soundInit("littleE");
soundInit("B");
soundInit("G");
soundInit("D");
soundInit("A");
soundInit("E");

function soundInit(i) {
    makeSound(440, i);
    oscillator[i].stop(0);
}
setInterval(function () {
    gainNode.gain.value = .5 * getValue("v") * gain;
    document.getElementById("q").innerHTML = "Volume:" + Math.round(getValue("v") * 100) + "%";
}, 100);

Can you Identify this song?

Grant Davis

Posted 2015-06-29T23:39:10.650

Reputation: 693

1It crashes on characters like | / b h p. Why not just do a little string parsing to replace them with -? That'll sound quite ok and it works. (And maybe split on newlines using one input box.). That'll make this a fun script to play with. – Matty – 2015-07-05T17:20:26.820

That what I was planning on doing, I just never got around to it. – Grant Davis – 2015-07-05T17:29:34.500

I agree, the different line for each string is a pain but otherwise it sounds good – Beta Decay – 2015-07-06T11:05:48.033

Oops forgot to login before editing the post. – Grant Davis – 2015-07-06T14:43:20.813

I recognise the tune but I can't put a name to it... Sounds cool though – Beta Decay – 2015-07-06T16:05:48.567

It does sound "cool"... It is Let it go from Frozen – Grant Davis – 2015-07-06T18:35:03.407

2

Java

This program converts a tablature to 16-bit WAV format.

Firstly, I wrote a whole bunch of tablature parsing code. I'm not sure if my parsing is entirely correct, but I think it's okay. Also, it could use more validation for data.

After that, I made the code to generate the audio. Each string is generated separately. The program keeps track of the current frequency, amplitude and phase. It then generates 10 overtones for the frequency with made-up relative amplitudes, and adds them up. Finally, the strings are combined and the result is normalized. The result is saved as WAV audio, which I chose for its ultra-simple format (no libraries used).

It "supports" hammering (h) and pulling (p) by ignoring them since I really didn't have the time to make them sound too different. The result sounds a bit like a guitar, though (spent some hours spectrum analyzing my guitar in Audacity).

Also, it supports bending (b), releasing (r) and sliding (/ and \, interchangeable). x is implemented as muting the string.

You can try tweaking the constants in the start of the code. Especially lowering silenceRate often leads to better quality.

Example results

The code

I want to warn any Java beginners: do not try to learn anything from this code, it's terribly written. Also, it was written quickly and in 2 sessions and it was not meant to be used ever again so it has no comments. (Might add some later :P)

import java.io.*;
import java.util.*;

public class TablatureSong {

    public static final int sampleRate = 44100;

    public static final double silenceRate = .4;

    public static final int harmonies = 10;
    public static final double harmonyMultiplier = 0.3;

    public static final double bendDuration = 0.25;

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.println("Output file:");
        String outfile = in.nextLine();
        System.out.println("Enter tablature:");
        Tab tab = parseTablature(in);
        System.out.println("Enter tempo:");
        int tempo = in.nextInt();
        in.close();

        int samples = (int) (60.0 / tempo * tab.length * sampleRate);
        double[][] strings = new double[6][];
        for (int i = 0; i < 6; i++) {
            System.out.printf("Generating string %d/6...\n", i + 1);
            strings[i] = generateString(tab.actions.get(i), tempo, samples);
        }

        System.out.println("Combining...");
        double[] combined = new double[samples];
        for (int i = 0; i < samples; i++)
            for (int j = 0; j < 6; j++)
                combined[i] += strings[j][i];

        System.out.println("Normalizing...");
        double max = 0;
        for (int i = 0; i < combined.length; i++)
            max = Math.max(max, combined[i]);
        for (int i = 0; i < combined.length; i++)
            combined[i] = Math.min(1, combined[i] / max);

        System.out.println("Writing file...");
        writeWaveFile(combined, outfile);
        System.out.println("Done");
    }

    private static double[] generateString(List<Action> actions, int tempo, int samples) {
        double[] harmonyPowers = new double[harmonies];
        for (int harmony = 0; harmony < harmonyPowers.length; harmony++) {
            if (Integer.toBinaryString(harmony).replaceAll("[^1]", "").length() == 1)
                harmonyPowers[harmony] = 2 * Math.pow(harmonyMultiplier, harmony);
            else
                harmonyPowers[harmony] = Math.pow(harmonyMultiplier, harmony);
        }
        double actualSilenceRate = Math.pow(silenceRate, 1.0 / sampleRate);

        double[] data = new double[samples];

        double phase = 0.0, amplitude = 0.0;
        double slidePos = 0.0, slideLength = 0.0;
        double startFreq = 0.0, endFreq = 0.0, thisFreq = 440.0;
        double bendModifier = 0.0;
        Iterator<Action> iterator = actions.iterator();
        Action next = iterator.hasNext() ? iterator.next() : new Action(Action.Type.NONE, Integer.MAX_VALUE);

        for (int sample = 0; sample < samples; sample++) {
            while (sample >= toSamples(next.startTime, tempo)) {
                switch (next.type) {
                case NONE:
                    break;
                case NOTE:
                    amplitude = 1.0;
                    startFreq = endFreq = thisFreq = next.value;
                    bendModifier = 0.0;
                    slidePos = 0.0;
                    slideLength = 0;
                    break;
                case BEND:
                    startFreq = addHalfSteps(thisFreq, bendModifier);
                    bendModifier = next.value;
                    slidePos = 0.0;
                    slideLength = toSamples(bendDuration);
                    endFreq = addHalfSteps(thisFreq, bendModifier);
                    break;
                case SLIDE:
                    slidePos = 0.0;
                    slideLength = toSamples(next.endTime - next.startTime, tempo);
                    startFreq = thisFreq;
                    endFreq = thisFreq = next.value;
                    break;
                case MUTE:
                    amplitude = 0.0;
                    break;
                }
                next = iterator.hasNext() ? iterator.next() : new Action(Action.Type.NONE, Integer.MAX_VALUE);
            }

            double currentFreq;
            if (slidePos >= slideLength || slideLength == 0)
                currentFreq = endFreq;
            else
                currentFreq = startFreq + (endFreq - startFreq) * (slidePos / slideLength);

            data[sample] = 0.0;
            for (int harmony = 1; harmony <= harmonyPowers.length; harmony++) {
                double phaseVolume = Math.sin(2 * Math.PI * phase * harmony);
                data[sample] += phaseVolume * harmonyPowers[harmony - 1];
            }

            data[sample] *= amplitude;
            amplitude *= actualSilenceRate;
            phase += currentFreq / sampleRate;
            slidePos++;
        }
        return data;
    }

    private static int toSamples(double seconds) {
        return (int) (sampleRate * seconds);
    }

    private static int toSamples(double beats, int tempo) {
        return (int) (sampleRate * beats * 60.0 / tempo);
    }

    private static void writeWaveFile(double[] data, String outfile) {
        try (OutputStream out = new FileOutputStream(new File(outfile))) {
            out.write(new byte[] { 0x52, 0x49, 0x46, 0x46 }); // Header: "RIFF"
            write32Bit(out, 44 + 2 * data.length, false); // Total size
            out.write(new byte[] { 0x57, 0x41, 0x56, 0x45 }); // Header: "WAVE"
            out.write(new byte[] { 0x66, 0x6d, 0x74, 0x20 }); // Header: "fmt "
            write32Bit(out, 16, false); // Subchunk1Size: 16
            write16Bit(out, 1, false); // Format: 1 (PCM)
            write16Bit(out, 1, false); // Channels: 1
            write32Bit(out, 44100, false); // Sample rate: 44100
            write32Bit(out, 44100 * 1 * 2, false); // Sample rate * channels *
                                                    // bytes per sample
            write16Bit(out, 1 * 2, false); // Channels * bytes per sample
            write16Bit(out, 16, false); // Bits per sample
            out.write(new byte[] { 0x64, 0x61, 0x74, 0x61 }); // Header: "data"
            write32Bit(out, 2 * data.length, false); // Data size
            for (int i = 0; i < data.length; i++) {
                write16Bit(out, (int) (data[i] * Short.MAX_VALUE), false); // Data
            }
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void write16Bit(OutputStream stream, int val, boolean bigEndian) throws IOException {
        int a = (val & 0xFF00) >> 8;
        int b = val & 0xFF;
        if (bigEndian) {
            stream.write(a);
            stream.write(b);
        } else {
            stream.write(b);
            stream.write(a);
        }
    }

    private static void write32Bit(OutputStream stream, int val, boolean bigEndian) throws IOException {
        int a = (val & 0xFF000000) >> 24;
        int b = (val & 0xFF0000) >> 16;
        int c = (val & 0xFF00) >> 8;
        int d = val & 0xFF;
        if (bigEndian) {
            stream.write(a);
            stream.write(b);
            stream.write(c);
            stream.write(d);
        } else {
            stream.write(d);
            stream.write(c);
            stream.write(b);
            stream.write(a);
        }
    }

    private static double[] strings = new double[] { 82.41, 110.00, 146.83, 196.00, 246.94, 329.63 };

    private static Tab parseTablature(Scanner in) {
        String[] lines = new String[6];
        List<List<Action>> result = new ArrayList<>();
        int longest = 0;
        for (int i = 0; i < 6; i++) {
            lines[i] = in.nextLine().trim().substring(2);
            longest = Math.max(longest, lines[i].length());
        }
        int skipped = 0;
        for (int i = 0; i < 6; i++) {
            StringIterator iterator = new StringIterator(lines[i]);
            List<Action> actions = new ArrayList<Action>();
            while (iterator.index() < longest) {
                if (iterator.get() < '0' || iterator.get() > '9') {
                    switch (iterator.get()) {
                    case 'b':
                        actions.add(new Action(Action.Type.BEND, 1, iterator.index(), iterator.index()));
                        iterator.next();
                        break;
                    case 'r':
                        actions.add(new Action(Action.Type.BEND, 0, iterator.index(), iterator.index()));
                        iterator.next();
                        break;
                    case '/':
                    case '\\':
                        int startTime = iterator.index();
                        iterator.findNumber();
                        int endTime = iterator.index();
                        int endFret = iterator.readNumber();
                        actions.add(new Action(Action.Type.SLIDE, addHalfSteps(strings[5 - i], endFret), startTime,
                                endTime));
                        break;
                    case 'x':
                        actions.add(new Action(Action.Type.MUTE, iterator.index()));
                        iterator.next();
                        break;
                    case '|':
                        iterator.skip(1);
                        iterator.next();
                        break;
                    case 'h':
                    case 'p':
                    case '-':
                        iterator.next();
                        break;
                    default:
                        throw new RuntimeException(String.format("Unrecognized character: '%c'", iterator.get()));
                    }
                } else {
                    StringBuilder number = new StringBuilder();
                    int startIndex = iterator.index();
                    while (iterator.get() >= '0' && iterator.get() <= '9') {
                        number.append(iterator.get());
                        iterator.next();
                    }
                    int fret = Integer.parseInt(number.toString());
                    double freq = addHalfSteps(strings[5 - i], fret);
                    actions.add(new Action(Action.Type.NOTE, freq, startIndex, startIndex));
                }
            }
            result.add(actions);
            skipped = iterator.skipped();
        }
        return new Tab(result, longest - skipped);
    }

    private static double addHalfSteps(double freq, double halfSteps) {
        return freq * Math.pow(2, halfSteps / 12.0);
    }

}

class StringIterator {
    private String string;
    private int index, skipped;

    public StringIterator(String string) {
        this.string = string;
        index = 0;
        skipped = 0;
    }

    public boolean hasNext() {
        return index < string.length() - 1;
    }

    public void next() {
        index++;
    }

    public void skip(int length) {
        skipped += length;
    }

    public char get() {
        if (index < string.length())
            return string.charAt(index);
        return '-';
    }

    public int index() {
        return index - skipped;
    }

    public int skipped() {
        return skipped;
    }

    public boolean findNumber() {
        while (hasNext() && (get() < '0' || get() > '9'))
            next();
        return get() >= '0' && get() <= '9';
    }

    public int readNumber() {
        StringBuilder number = new StringBuilder();
        while (get() >= '0' && get() <= '9') {
            number.append(get());
            next();
        }
        return Integer.parseInt(number.toString());
    }
}

class Action {
    public static enum Type {
        NONE, NOTE, BEND, SLIDE, MUTE;
    }

    public Type type;
    public double value;
    public int startTime, endTime;

    public Action(Type type, int time) {
        this(type, time, time);
    }

    public Action(Type type, int startTime, int endTime) {
        this(type, 0, startTime, endTime);
    }

    public Action(Type type, double value, int startTime, int endTime) {
        this.type = type;
        this.value = value;
        this.startTime = startTime;
        this.endTime = endTime;
    }
}

class Tab {
    public List<List<Action>> actions;
    public int length;

    public Tab(List<List<Action>> actions, int length) {
        this.actions = actions;
        this.length = length;
    }
}

PurkkaKoodari

Posted 2015-06-29T23:39:10.650

Reputation: 16 699

I know I haven't specified it, but could you post some test cases that people can listen to like in the other answers? – Beta Decay – 2015-07-09T20:35:40.510

@BetaDecay Updated my answer, now has a bunch of tests – PurkkaKoodari – 2015-07-09T21:03:21.900

Those links don't work :/ – Beta Decay – 2015-07-09T21:07:02.723

@BetaDecay I double checked on another connection in incognito mode of a browser I don't use. They work for me, at least. – PurkkaKoodari – 2015-07-09T21:29:36.960

Nice, I like your version of Mattys tune a lot, although the base is sometimes hard to hear. – Matty – 2015-07-09T21:34:37.390