Jawa
Ten program konwertuje tabulatury do 16-bitowego formatu WAV.
Po pierwsze, napisałem całą masę kodu do analizy tabulatury. Nie jestem pewien, czy parsowanie jest całkowicie poprawne, ale myślę, że jest w porządku. Ponadto może użyć większej weryfikacji danych.
Następnie stworzyłem kod do generowania dźwięku. Każdy ciąg jest generowany osobno. Program śledzi aktualną częstotliwość, amplitudę i fazę. Następnie generuje 10 nadtonów dla częstotliwości z utworzonymi amplitudami względnymi i sumuje je. Na koniec ciągi są łączone, a wynik jest znormalizowany. Rezultat jest zapisywany jako dźwięk WAV, który wybrałem ze względu na jego bardzo prosty format (bez bibliotek).
„Obsługuje” hammering ( h
) i pull ( p
), ignorując je, ponieważ tak naprawdę nie miałem czasu, aby brzmiały zbyt odmiennie. Rezultat brzmi trochę jak gitara (spędziłem kilka godzin na analizie mojej gitary w Audacity).
Obsługuje również gięcie ( b
), zwalnianie ( r
) i przesuwanie ( /
i \
, wymiennie). x
jest implementowany jako wyciszenie łańcucha.
Możesz spróbować ulepszyć stałe na początku kodu. Szczególnie obniżenie silenceRate
często prowadzi do lepszej jakości.
Przykładowe wyniki
Kod
Chcę ostrzec wszystkich początkujących w Javie: nie próbuj niczego się uczyć z tego kodu, jest strasznie napisany. Ponadto został napisany szybko i w 2 sesjach i nie miał być ponownie używany, więc nie ma komentarzy. (Może dodać trochę później: 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;
}
}