THX Deep Note Generator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
MIT License | |
Copyright (c) 2018 Jonas Balsfulland | |
Permission is hereby granted, free of charge, to any person obtaining a copy | |
of this software and associated documentation files (the "Software"), to deal | |
in the Software without restriction, including without limitation the rights | |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
copies of the Software, and to permit persons to whom the Software is | |
furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all | |
copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
SOFTWARE. | |
*/ | |
import java.io.ByteArrayInputStream; | |
import java.io.File; | |
import java.util.Arrays; | |
import java.util.Random; | |
import javax.sound.sampled.AudioFileFormat; | |
import javax.sound.sampled.AudioFormat; | |
import javax.sound.sampled.AudioInputStream; | |
import javax.sound.sampled.AudioSystem; | |
import javax.sound.sampled.SourceDataLine; | |
/** | |
* THX Deep Note Sheet music | |
* https://i.kinja-img.com/gawker-media/image/upload/s--MHW4C38s--/c_scale,f_auto,fl_progressive,q_80,w_800/p2emaasjrbxi4tbhreyr.png | |
* @author Jonas | |
*/ | |
public class THX { | |
public static final int SAMPLE_RATE = 128 * 1024; | |
private static double state = 0.0f; | |
private static double state11 = -1.0; | |
private static double state12 = -1.0; | |
private static double state13 = -1.0; | |
private static double state2 = -1.0; | |
private static double sign = 1.0f; | |
public static void main(String[] args) throws Exception { | |
File out = new File("out.wav"); | |
final AudioFormat af = new AudioFormat(SAMPLE_RATE, 16, 1, true, true); | |
SourceDataLine line = AudioSystem.getSourceDataLine(af); | |
byte[] tone = generateTHX(); | |
line.open(af, SAMPLE_RATE); | |
line.start(); | |
play(line, tone); | |
line.drain(); | |
line.close(); | |
//save(tone, af, out); | |
} | |
private static byte[] generateTHX() throws Exception { | |
System.out.println("Generating THX Deep Note"); | |
int[] targetAccord = { | |
1480, 1480, 1480, | |
1175, 1175, 1175, | |
880, 880, 880, | |
587, 587, 587, | |
440, 440, 440, | |
294, 294, 294, | |
220, 220, | |
147, 147, | |
110, 110, | |
73, 73, | |
55, 55 | |
}; | |
for (int i = 0; i < 18; i++) { | |
double mistake = 0.01; | |
targetAccord[i] *= 1 + (Math.random() * mistake - (mistake / 2)); | |
} | |
int lineCount = targetAccord.length; | |
int speed = 4000; | |
int min = 200; | |
int max = 400; | |
int minSteps = 3; | |
int maxSteps = 10; | |
int durationRandom = speed * 3; | |
int durationWalk = speed * 2; | |
int durationHold = speed * 1; | |
int durationFade = speed * 1; | |
int maxChange = 50; | |
double[][] random = new double[lineCount][]; | |
double[][] accordWalk = new double[lineCount][]; | |
double[][] accordHold = new double[lineCount][]; | |
double[][] accordFade = new double[lineCount][]; | |
int[][] randomWalks = new int[lineCount][]; | |
for (int i = 0; i < lineCount; i++) { | |
int steps = new Random().nextInt(maxSteps - minSteps) + minSteps; | |
randomWalks[i] = randomWalk(steps, min, max, maxChange); | |
} | |
int[][] accordWalks = new int[lineCount][2]; | |
for (int i = 0; i < lineCount; i++) { | |
accordWalks[i] = new int[]{randomWalks[i][randomWalks[i].length - 1], targetAccord[i]}; | |
} | |
int[][] accordHolds = new int[lineCount][2]; | |
for (int i = 0; i < lineCount; i++) { | |
accordHolds[i] = new int[]{targetAccord[i], targetAccord[i]}; | |
} | |
//Triangle waves sound best | |
for (int i = 0; i < lineCount; i++) { | |
System.out.println("Generating line " + (i + 1) + " / " + lineCount); | |
state = -1.0; | |
sign = 1.0; | |
random[i] = generateTone(randomWalks[i], durationRandom); | |
accordWalk[i] = generateTone(accordWalks[i], durationWalk); | |
accordHold[i] = generateTone(accordHolds[i], durationHold); | |
accordFade[i] = generateTone(accordHolds[i], durationFade); | |
} | |
System.out.println("Merging lines"); | |
double[] randomMerged = merge(random); | |
double[] accordWalkMerged = merge(accordWalk); | |
double[] accordHoldMerged = merge(accordHold); | |
double[] accordFadeMerged = merge(accordFade); | |
System.out.println("Filtering noise"); | |
float filterStart = 2000f; | |
float filterEnd = 6000f; | |
float walkFilterPercentageStart = 0.5f; | |
Filter filter = new Filter(filterStart, SAMPLE_RATE, Filter.PassType.Lowpass, 1); | |
for (int i = 0; i < randomMerged.length; i++) { | |
filter.update(randomMerged[i]); | |
randomMerged[i] = filter.getValue(); | |
} | |
for (int i = 0; i < accordWalkMerged.length; i++) { | |
if(i >= accordWalkMerged.length * walkFilterPercentageStart) { | |
int c = (int)(i - accordWalkMerged.length * walkFilterPercentageStart); | |
float next = filterStart + (filterEnd - filterStart) * ((float) c / (float) (accordWalkMerged.length * walkFilterPercentageStart)); | |
filter.changeFrequency(next); | |
} | |
filter.update(accordWalkMerged[i]); | |
accordWalkMerged[i] = filter.getValue(); | |
} | |
for (int i = 0; i < accordHoldMerged.length; i++) { | |
filter.update(accordHoldMerged[i]); | |
accordHoldMerged[i] = filter.getValue(); | |
} | |
for (int i = 0; i < accordFadeMerged.length; i++) { | |
filter.update(accordFadeMerged[i]); | |
accordFadeMerged[i] = filter.getValue(); | |
} | |
double[] rising = concat(randomMerged, accordWalkMerged, accordHoldMerged); | |
System.out.println("Scaling lines"); | |
scaleDouble(accordFadeMerged, 0x7fff, 0); | |
scaleDouble(rising, 1, 0x7fff); | |
double[] dTone = concat(rising, accordFadeMerged); | |
System.out.println("Generating final audio"); | |
byte[] tone = doubleToBytesDirect(dTone); | |
return tone; | |
} | |
private static void scaleDouble(double[] arr, int start, int end) { | |
for (int i = 0; i < arr.length; i++) { | |
double volume = start + (end - start) * ((double) i / (double) arr.length); | |
arr[i] *= volume; | |
} | |
} | |
private static byte[] doubleToBytesDirect(double[] arr) { | |
byte[] data = new byte[arr.length * 2]; | |
for (int i = 0; i < arr.length; i++) { | |
int masked = (int) arr[i]; | |
data[i * 2 + 0] = (byte) ((masked >> 8) & 0xff); | |
data[i * 2 + 1] = (byte) ((masked >> 0) & 0xff); | |
} | |
return data; | |
} | |
private static int[] randomWalk(int steps, int min, int max, int maxChange) { | |
Random rand = new Random(); | |
int[] arr = new int[steps]; | |
arr[0] = rand.nextInt(max - min) + min; | |
for (int i = 1; i < arr.length; i++) { | |
int change = rand.nextInt(2 * maxChange) - maxChange; | |
arr[i] = Math.max(min, Math.min(max, arr[i - 1] + change)); | |
} | |
return arr; | |
} | |
public static double[] concat(double[]... tones) { | |
int sum = Arrays.stream(tones).mapToInt(a -> a.length).sum(); | |
double[] tone = new double[sum]; | |
int offset = 0; | |
for (int i = 0; i < tones.length; i++) { | |
System.arraycopy(tones[i], 0, tone, offset, tones[i].length); | |
offset += tones[i].length; | |
} | |
return tone; | |
} | |
public static double[] merge(double[]... tones) { | |
int min = Arrays.stream(tones).mapToInt(a -> a.length).min().orElse(0); | |
double[] tone = new double[min]; | |
for (int i = 0; i < min; i++) { | |
for (int j = 0; j < tones.length; j++) { | |
tone[i] += tones[j][i]; | |
} | |
tone[i] = (tone[i] / tones.length); | |
} | |
return tone; | |
} | |
public static double[] generateTone(int[] freq, int msDuration) { | |
double[] sin = new double[SAMPLE_RATE * (msDuration / 1000)]; // parens to avoid arithmetic overflow | |
int fac = 1 + sin.length / (freq.length - 1); | |
for (int j = -1; j < sin.length + 1; j++) { | |
// sawtooth oscillator 1 | |
int i = (j - 1) / fac; | |
double f = freq[i] + (freq[i + 1] - freq[i]) * ((double) ((j - 1) % fac) / (double) fac); | |
double period = (double) SAMPLE_RATE / (2 * f); | |
state11 += 1 / period; | |
if (state11 > 1.0) { | |
state11 = -1.0; | |
} | |
double osc11 = state11; | |
// sawtooth oscillator 2 | |
i = j / fac; | |
f = freq[i] + (freq[i + 1] - freq[i]) * ((double) (j % fac) / (double) fac); | |
period = (double) SAMPLE_RATE / (2 * f); | |
state12 += 1 / period; | |
if (state12 > 1.0) { | |
state12 = -1.0; | |
} | |
double osc12 = state12; | |
// sawtooth oscillator 5 | |
i = j / fac; | |
f = freq[i] + (freq[i + 1] - freq[i]) * ((double) (j % fac) / (double) fac); | |
period = (double) SAMPLE_RATE / (2 * f); | |
state13 += 1 / period; | |
if (state13 > 1.0) { | |
state13 = -1.0; | |
} | |
double osc13 = state13; | |
// take average of two saw oscillator to filter high freq | |
double osc1 = osc11 * 0.25 + osc12 * 0.5 + osc13 * 0.25; | |
// sine oscillator | |
i = j / fac; | |
f = freq[i] + (freq[i + 1] - freq[i]) * ((double) (j % fac) / (double) fac); | |
period = (double) SAMPLE_RATE / f; | |
state2 += 2.0 * Math.PI / period; | |
Math.sin(state2); | |
double osc2 = Math.sin(state2); | |
if (j >= 0 && j < sin.length) { | |
sin[j] = 0.4 * osc1 + 0.6 * osc2; | |
} | |
} | |
return sin; | |
} | |
public static double[] generateTriangle(int[] freq, int msDuration) { | |
double[] sin = new double[SAMPLE_RATE * msDuration / 1000]; | |
int fac = 1 + sin.length / (freq.length - 1); | |
for (int j = 0; j < sin.length; j++) { | |
int i = j / fac; | |
double f = freq[i] + (freq[i + 1] - freq[i]) * ((double) (j % fac) / (double) fac); | |
double period = (double) SAMPLE_RATE / (f * 4); | |
state += sign / period; | |
if (state > 1.0 || state < -1.0) { | |
state = sign; | |
sign *= -1; | |
} | |
sin[j] = state; | |
} | |
return sin; | |
} | |
public static double[] generateSawtooth(int[] freq, int msDuration) { | |
double[] sin = new double[SAMPLE_RATE * msDuration / 1000]; | |
int fac = 1 + sin.length / (freq.length - 1); | |
for (int j = 0; j < sin.length; j++) { | |
int i = j / fac; | |
double f = freq[i] + (freq[i + 1] - freq[i]) * ((double) (j % fac) / (double) fac); | |
double period = (double) SAMPLE_RATE / (2 * f); | |
state += 1 / period; | |
if (state > 1.0) { | |
state = -1.0; | |
} | |
sin[j] = state; | |
} | |
return sin; | |
} | |
public static double[] generateSquare(int[] freq, int msDuration) { | |
double[] sin = new double[SAMPLE_RATE * msDuration / 1000]; | |
int fac = 1 + sin.length / (freq.length - 1); | |
for (int j = 0; j < sin.length; j++) { | |
int i = j / fac; | |
double f = freq[i] + (freq[i + 1] - freq[i]) * ((double) (j % fac) / (double) fac); | |
double period = (double) SAMPLE_RATE / f; | |
state += 2.0 * Math.PI / period; | |
double v = Math.sin(state); | |
if (v < -0.5) { | |
v = -1; | |
} else if (v > 0.5) { | |
v = 1; | |
} | |
sin[j] = v; | |
} | |
return sin; | |
} | |
public static double[] generateSine(int[] freq, int msDuration) { | |
double[] sin = new double[SAMPLE_RATE * msDuration / 1000]; | |
int fac = 1 + sin.length / (freq.length - 1); | |
for (int j = 0; j < sin.length; j++) { | |
int i = j / fac; | |
double f = freq[i] + (freq[i + 1] - freq[i]) * ((double) (j % fac) / (double) fac); | |
double period = (double) SAMPLE_RATE / f; | |
state += 2.0 * Math.PI / period; | |
sin[j] = Math.sin(state); | |
} | |
return sin; | |
} | |
public static void play(SourceDataLine line, byte[] tone) { | |
line.write(tone, 0, tone.length); | |
} | |
public static void save(byte[] tone, AudioFormat format, File file) throws Exception { | |
ByteArrayInputStream bais = new ByteArrayInputStream(tone); | |
AudioInputStream ais = new AudioInputStream(bais, format, tone.length); | |
AudioSystem.write(ais, AudioFileFormat.Type.WAVE, file); | |
} | |
//https://stackoverflow.com/questions/28252665/how-to-implement-a-high-pass-filter-for-an-audio-signal | |
private static class Filter { | |
private final float resonance; | |
private final int sampleRate; | |
private final PassType passType; | |
public double value; | |
private double c, a1, a2, a3, b1, b2; | |
private double[] inputHistory = new double[2]; | |
private double[] outputHistory = new double[3]; | |
public Filter(float frequency, int sampleRate, PassType passType, float resonance) { | |
this.sampleRate = sampleRate; | |
this.passType = passType; | |
this.resonance = resonance; | |
changeFrequency(frequency); | |
} | |
public enum PassType { | |
Highpass, | |
Lowpass, | |
} | |
public void changeFrequency(float frequency) { | |
switch (passType) { | |
case Lowpass: | |
c = 1.0f / (float) Math.tan(Math.PI * frequency / sampleRate); | |
a1 = 1.0f / (1.0f + resonance * c + c * c); | |
a2 = 2f * a1; | |
a3 = a1; | |
b1 = 2.0f * (1.0f - c * c) * a1; | |
b2 = (1.0f - resonance * c + c * c) * a1; | |
break; | |
case Highpass: | |
c = (float) Math.tan(Math.PI * frequency / sampleRate); | |
a1 = 1.0f / (1.0f + resonance * c + c * c); | |
a2 = -2f * a1; | |
a3 = a1; | |
b1 = 2.0f * (c * c - 1.0f) * a1; | |
b2 = (1.0f - resonance * c + c * c) * a1; | |
break; | |
} | |
} | |
public void update(double newInput) { | |
double newOutput = a1 * newInput + a2 * this.inputHistory[0] + a3 * this.inputHistory[1] - b1 * this.outputHistory[0] - b2 * this.outputHistory[1]; | |
this.inputHistory[1] = this.inputHistory[0]; | |
this.inputHistory[0] = newInput; | |
this.outputHistory[2] = this.outputHistory[1]; | |
this.outputHistory[1] = this.outputHistory[0]; | |
this.outputHistory[0] = newOutput; | |
} | |
public double getValue() { | |
return this.outputHistory[0]; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
https://joba.me/public/img/thx.wav