From acb8aa001ca67ee837dbddfb6a9f9adaa1f3f5f9 Mon Sep 17 00:00:00 2001 From: aleigh Date: Wed, 13 Apr 2022 11:54:57 -0700 Subject: [PATCH] leigh-audio-bpm: Added BPM analysis code from https://github.com/widget-/bpm-detect MIT license --- leigh-audio-bpm/build.gradle | 21 + .../java/leigh/audio/bpm/BPMBestGuess.java | 80 ++++ .../main/java/leigh/audio/bpm/BPMDetect.java | 271 ++++++++++++ .../java/leigh/audio/bpm/ColorProgram.java | 69 +++ .../java/leigh/audio/bpm/ColorPrograms.java | 413 ++++++++++++++++++ .../java/leigh/audio/bpm/ColorScheme.java | 49 +++ .../leigh/audio/bpm/ColoredScrollbar.java | 83 ++++ .../src/main/java/leigh/audio/bpm/FFTer.java | 89 ++++ .../src/main/java/leigh/audio/bpm/Light.java | 133 ++++++ .../java/leigh/audio/bpm/LightCollection.java | 100 +++++ .../java/leigh/audio/bpm/LightMessage.java | 12 + .../src/main/java/leigh/audio/bpm/Pair.java | 11 + .../java/leigh/audio/bpm/SampleHistory.java | 85 ++++ .../java/leigh/audio/bpm/SerialCache.java | 70 +++ .../java/leigh/audio/bpm/SerialWriter.java | 159 +++++++ .../java/leigh/audio/bpm/WaveformPanel.java | 88 ++++ .../java/leigh/audio/bpm/pgm_ActiveSolid.java | 42 ++ settings.gradle | 1 + 18 files changed, 1776 insertions(+) create mode 100644 leigh-audio-bpm/build.gradle create mode 100644 leigh-audio-bpm/src/main/java/leigh/audio/bpm/BPMBestGuess.java create mode 100644 leigh-audio-bpm/src/main/java/leigh/audio/bpm/BPMDetect.java create mode 100644 leigh-audio-bpm/src/main/java/leigh/audio/bpm/ColorProgram.java create mode 100644 leigh-audio-bpm/src/main/java/leigh/audio/bpm/ColorPrograms.java create mode 100644 leigh-audio-bpm/src/main/java/leigh/audio/bpm/ColorScheme.java create mode 100644 leigh-audio-bpm/src/main/java/leigh/audio/bpm/ColoredScrollbar.java create mode 100644 leigh-audio-bpm/src/main/java/leigh/audio/bpm/FFTer.java create mode 100644 leigh-audio-bpm/src/main/java/leigh/audio/bpm/Light.java create mode 100644 leigh-audio-bpm/src/main/java/leigh/audio/bpm/LightCollection.java create mode 100644 leigh-audio-bpm/src/main/java/leigh/audio/bpm/LightMessage.java create mode 100644 leigh-audio-bpm/src/main/java/leigh/audio/bpm/Pair.java create mode 100644 leigh-audio-bpm/src/main/java/leigh/audio/bpm/SampleHistory.java create mode 100644 leigh-audio-bpm/src/main/java/leigh/audio/bpm/SerialCache.java create mode 100644 leigh-audio-bpm/src/main/java/leigh/audio/bpm/SerialWriter.java create mode 100644 leigh-audio-bpm/src/main/java/leigh/audio/bpm/WaveformPanel.java create mode 100644 leigh-audio-bpm/src/main/java/leigh/audio/bpm/pgm_ActiveSolid.java diff --git a/leigh-audio-bpm/build.gradle b/leigh-audio-bpm/build.gradle new file mode 100644 index 000000000..33e523ed2 --- /dev/null +++ b/leigh-audio-bpm/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'java' + id 'java-library' +} + +group 'leigh' +version '1.0' + +repositories { + mavenCentral() +} + +dependencies { + api 'com.github.wendykierp:JTransforms:3.1' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/leigh-audio-bpm/src/main/java/leigh/audio/bpm/BPMBestGuess.java b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/BPMBestGuess.java new file mode 100644 index 000000000..5f4700e8c --- /dev/null +++ b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/BPMBestGuess.java @@ -0,0 +1,80 @@ +package leigh.audio.bpm; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map.Entry; + + +public class BPMBestGuess { + + private HashMap bpmEntries = new HashMap(); + + private static double DECAY_RATE = 0.999; + private static double DELETE_THRESHHOLD = 0.01; + private double confidence = 0; + + public BPMBestGuess() { + } + + + public void appendBPMGuess(double bpm, double confidence) { + Iterator> it = bpmEntries.entrySet().iterator(); + while (it.hasNext()) { + Entry e = it.next(); + e.setValue(e.getValue() * DECAY_RATE); + if (e.getValue() < DELETE_THRESHHOLD) + it.remove(); + } + if (BPMDetect.isBreakdown()) + return; + if (bpmEntries.containsKey(bpm)) + bpmEntries.put(bpm, bpmEntries.get(bpm) + confidence); + else + bpmEntries.put(bpm, confidence); + } + + private double calculateGuess() { + double bestGuessStart = 0; + double bestGuessValue = 0; + double currentGuessStart = 0; + double currentGuessValue = 0; + boolean rising = false; +// Entry lastEntry = new SimpleImmutableEntry(0d, 0d); +// for (Entry e : bpmEntries.entrySet()) { +// if (rising) +// if (lastEntry.getValue() < e.getValue()) +// currentGuessValue += e.getValue(); +// else +// rising = false; +// else +// if (lastEntry.getValue() > e.getValue()) +// currentGuessValue += e.getValue(); +// else { +// rising = true; +// if (currentGuessValue > bestGuessValue) +// bestGuessValue = currentGuessValue; +// bestGuessStart = currentGuessStart; +// currentGuessStart = e.getKey(); +// } +// } + for (Entry e : bpmEntries.entrySet()) { +// System.out.println(e.getKey()+"\t"+e.getValue()); + if (e.getValue() > bestGuessValue) { + bestGuessStart = e.getKey(); + bestGuessValue = e.getValue(); + } + } +// System.out.println("---------------------"); + + confidence = bestGuessValue; + return bestGuessStart; + } + + public double getConfidence() { + return this.confidence; + } + + public double getBPM() { + return calculateGuess(); + } +} diff --git a/leigh-audio-bpm/src/main/java/leigh/audio/bpm/BPMDetect.java b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/BPMDetect.java new file mode 100644 index 000000000..b597fb6a1 --- /dev/null +++ b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/BPMDetect.java @@ -0,0 +1,271 @@ +package leigh.audio.bpm; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map.Entry; + + +public class BPMDetect { + + public static final int NUM_FLAGS = 8; + + // public static Multimap reps = TreeMultimap.create(); + public static List bflags = new ArrayList(); + public static List mflags = new ArrayList(); + public static List tflags = new ArrayList(); + public static double[] bpm = {-1, -1, -1}; + public static double[] confidence = {0, 0, 0}; + + private static double[] shortAmplitudes = new double[3]; + private static double[] longAmplitudes = new double[3]; + + private static int ramp = GUI.sampleRate / 60; + public static double rampRequired = 0.35; + private static int rampSeperation = ramp * 5; + + private static long downbeatTime = System.currentTimeMillis(); + + private static SampleHistory sh; + + public BPMDetect(SampleHistory sh) { + } + + public static void setSampleSource(SampleHistory sh) { + BPMDetect.sh = sh; + } + + public static double detectBPM(float[] samples, int sampleRate, int sampleType) { + for (int flagType = 0; flagType < 3; flagType++) { + List flags = null; + switch (sampleType) { + case 0: + flags = bflags; + break; + case 1: + flags = mflags; + break; + case 2: + flags = tflags; + break; + } + + flags.clear(); + + int crossoverSize = sampleRate * 60 / 130 * 4; // 4 beats at 130BPM + if (crossoverSize > sampleRate * GUI.historySeconds) + return -1; // whoops + + float max = SampleHistory.getMaxInArray(samples); + int lastBeat = 0; + for (int i = 1; i < samples.length; i++) { + if ((samples[i] - samples[i - 1]) / max > rampRequired) { + if (i - lastBeat < rampSeperation) + continue; + else + lastBeat = i; + flags.add(i - 1); + } + } + + shortAmplitudes[sampleType] = 0; + for (int i = samples.length - (int) (GUI.sampleRate * 0.3); i < samples.length - 1; i++) + shortAmplitudes[sampleType] += samples[i]; + longAmplitudes[sampleType] = 0; + for (int i = samples.length - GUI.sampleRate * 5; i < samples.length - 1; i++) + longAmplitudes[sampleType] += samples[i]; + + bpm[sampleType] = bpmFromFlags(sampleRate, sampleType); + + if (bpm[sampleType] > 0) { + while (bpm[sampleType] > 180) + bpm[sampleType] /= 2.0; + while (bpm[sampleType] < 90) + bpm[sampleType] *= 2.0; + } + } + + + GUI.getBPM().appendBPMGuess(bpm[sampleType], confidence[sampleType]); + + return bpm[sampleType]; + } + + public static double bpmFromFlags(int sampleRate, int sampleType) { + ArrayList distances = new ArrayList(); + List flags = null; + switch (sampleType) { + case 0: + flags = bflags; + break; + case 1: + flags = mflags; + break; + case 2: + flags = tflags; + break; + } + + for (int i = 0; i < flags.size(); i++) + for (int j = i + 1; j < flags.size(); j++) { + int distance = Math.abs(flags.get(i) - flags.get(j)); + // while (distance > (sampleRate * 60 / 180)) + // distance /= 2; + distances.add(distance); + // System.out.print(distance+"|"); + } + HashMap distanceMap = new HashMap(); + for (int distance : distances) { + for (int i = -2; i < 3; i++) + if (distanceMap.containsKey(distance + i)) + distanceMap.put(distance + i, distanceMap.get(distance + i) + 1 - Math.abs(i) / 4.0); + else + distanceMap.put(distance + i, 1 - Math.abs(i) / 4.0); + // System.out.print(distance+" "); + } + + // System.out.println(distanceMap.size()); + Pair bestGuess = new Pair(-1, -1.0); + // Pair nextBestGuess = new Pair(-1, -1.0); + int scoreTotal = 0; + for (Entry e : distanceMap.entrySet()) { + if (e.getValue() > bestGuess.y) { + // nextBestGuess = bestGuess; + bestGuess = new Pair(e.getKey(), e.getValue()); + scoreTotal += e.getValue(); + } + // System.out.print("K: "+e.getKey()+",V:"+e.getValue()+". "); + } + // System.out.println(); + + // confidence[sampleType] = 15.0 / Math.min(15, flags.size()) * 100; + // confidence[sampleType] = ((bestGuess.y / nextBestGuess.y)) * 50; + // confidence[sampleType] = ((bestGuess.y / nextBestGuess.y)); + confidence[sampleType] = bestGuess.y / scoreTotal * 100; + confidence[sampleType] = Math.min((confidence[sampleType] - 10) * (100f / 25), 100); + if (flags.size() == 0) + confidence[sampleType] = 0; + else if (flags.size() < 10) + confidence[sampleType] *= flags.size() / 10; + if (bestGuess.x == 0) + return -1; + else { + double bpm = sampleRate * 60.0 / (bestGuess.x - 1); + if (bpm <= 0) + return bpm; + while (bpm < 70) + bpm *= 2.0; + while (bpm > 180) + bpm /= 2.0; + return bpm; + } + } + + public static boolean isBreakdown() { + // if (!wasBreakdown) { + // wasBreakdown = (shortAmplitudes[0] < (shortAmplitudes[1] + shortAmplitudes[2]) * 0.4d) && (longAmplitudes[0] < (longAmplitudes[1] + longAmplitudes[2]) * 0.4d); + // return wasBreakdown; + // } else { + // wasBreakdown = (shortAmplitudes[0] < (shortAmplitudes[1] + shortAmplitudes[2]) * 0.6d) && (longAmplitudes[0] < (longAmplitudes[1] + longAmplitudes[2]) * 0.6d); + // return wasBreakdown; + // } + try { + float[] samples = sh.getFftSamples(SampleHistory.FFT_BASS); + + int flips = 0; + boolean peaked = false; + for (int i = samples.length * 2 / 3; i < samples.length; i++) { + if (peaked) { + if (samples[i] < FFTer.avgBassLow) { + peaked = false; + } + } else { + if (samples[i] > FFTer.avgBassHigh) { + peaked = true; + flips++; + } + } + } + return (flips < 5); + } catch (Exception e) { + return true; + } + } + + public static long getDownbeat(float[] samples, int sampleRate) { + Pair bestGuess = new Pair(0, 0f); + float[] sums = new float[samples.length - sampleRate]; + for (int i = 0; i < samples.length - sampleRate; i++) + for (int j = 0; j < sampleRate; j++) + sums[i] += samples[i + j]; + for (int i = 0; i < samples.length - 2 * sampleRate; i++) + if (sums[i + sampleRate] - sums[i] > bestGuess.y) + bestGuess = new Pair(i + sampleRate, sums[i + sampleRate]); + + if (sums[bestGuess.x - sampleRate] * 5 < sums[bestGuess.x]) + if (Math.abs(downbeatTime - (System.currentTimeMillis() - (samples.length - bestGuess.x) * 1000 / sampleRate)) > 100) + downbeatTime = System.currentTimeMillis() - (samples.length - bestGuess.x) * 1000 / sampleRate; + + return downbeatTime; + } + + public static int getBeat() { + double time = (System.currentTimeMillis() - downbeatTime); + double bpm = GUI.getBPM().getBPM(); + + long beat = Math.round(bpm * time / 60000); + + return (int) beat % 32 + 1; + } + + public static double getPhase() { + double time = (System.currentTimeMillis() - downbeatTime); + double bpm = GUI.getBPM().getBPM(); + return (bpm * time / 60000) - Math.round(bpm * time / 60000) + 0.5d; + } + + public static float[] filterSamplesDecay(float[] samples) { + final int DECAY_LOOKBACK = 5; + boolean keepGoing = true; + int cnt = 0; + float[] s = new float[samples.length]; + s = samples.clone(); + // for (int i = samples.length - 1; i >= 0; i--) { + // while (keepGoing) { + // if (cnt < DECAY_LOOKBACK) { + // if (i - cnt > 0 && s[i-cnt] < s[i]) { + // s[i] = s[i-cnt]; + // } else { + // cnt++; + // } + // } else { + // cnt = 0; + // keepGoing = false; + // } + // } + // keepGoing = true; + // cnt = 0; + // } + for (int i = s.length - 1; i > 0; i--) + for (int j = 0; j < 5; j++) + if (i > 5 && s[i] < s[i - j]) + s[i] = s[i - j]; + // s[s.length - 1] = s[s.length - 2]; + return s; + } + + public static float[] dIntegral(float[] samples) { + float[] s = new float[samples.length]; + s[0] = 0; + for (int i = 1; i < s.length; i++) + if (samples[i] - samples[i - 1] > 0) + s[i] = samples[i] - samples[i - 1]; + else + s[i] = 0; + return s; + } + + public static float[] filteredIntegral(float[] samples) { + return dIntegral(filterSamplesDecay(samples)); + } +} diff --git a/leigh-audio-bpm/src/main/java/leigh/audio/bpm/ColorProgram.java b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/ColorProgram.java new file mode 100644 index 000000000..01d37421f --- /dev/null +++ b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/ColorProgram.java @@ -0,0 +1,69 @@ +package leigh.audio.bpm; + +import java.awt.*; + + +/** + * + */ + +/** @author Widget */ +public abstract class ColorProgram { + + protected int beat; + protected double phase; + protected double bass, mid, treb; + + protected int programID; + protected String programName; + + private int r, g, b; + + + public ColorProgram() { + + } + + public abstract Color getColor(Light light, ColorScheme colorScheme); + + public abstract String getProgramName(); + + public abstract int getProgramID(); + + public void updateParameters(int beat, double phase, double bass, double mid, double treb) { + this.beat = beat; + this.phase = phase; + this.bass = bass; + this.mid = mid; + this.treb = treb; + } + + public void updateStaticColors(int r, int g, int b) { + // please someone explain why i have to flip g and b for this to be right + this.r = r; + this.g = b; + this.b = g; + } + + + protected Color shiftColor(Color color, float h, float s, float b) { + float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null); + hsb[0] += h; + hsb[1] = Math.min(Math.max(hsb[1] + s, 0f), 1f); + hsb[2] = Math.min(Math.max(hsb[2] + b, 0f), 1f); + return new Color(Color.HSBtoRGB(hsb[0], hsb[1], hsb[2])); + } + + protected Color scaleRGB(Color color) { + float[] rgb = new float[3]; + color.getColorComponents(rgb); + rgb[0] = (rgb[0] * r) / 255f; + rgb[1] = (rgb[1] * g) / 255f; + rgb[2] = (rgb[2] * b) / 255f; + return new Color(rgb[0], rgb[1], rgb[2]); + } + + public Color getStaticColor() { + return new Color(r / 255f, g / 255f, b / 255f); + } +} diff --git a/leigh-audio-bpm/src/main/java/leigh/audio/bpm/ColorPrograms.java b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/ColorPrograms.java new file mode 100644 index 000000000..29b5f4ec8 --- /dev/null +++ b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/ColorPrograms.java @@ -0,0 +1,413 @@ +package leigh.audio.bpm; + +import javafx.geometry.Point2D; + +import java.awt.*; + + +public class ColorPrograms { + + public static enum Program { + ACTIVE_SOLID, + ACTIVE_PULSE, + ACTIVE_FADE, + ACTIVE_SPIN, + ACTIVE_CHASE1, + ACTIVE_CHASE2, + ACTIVE_EXPLODE, + ACTIVE_DIAMOND, + ACTIVE_FROMPT, + ACTIVE_FROMPT2, + PASSIVE_DFADE, + PASSIVE_HFADE, + PASSIVE_FLATFADE, + PASSIVE_FMOSAIC, + RESPONSE_ACROSS, + RESPONSE_ALONG + } + + + private ColorScheme colorScheme = new ColorScheme(); + private Program currentProgram = Program.PASSIVE_DFADE; + + private boolean specificProgram = false; + + private static boolean wasBreakdown = false; + + private int beat = 0; + private int lastBeat = 0; + private double phase = 0; + + private double bass = 0; + private double mid = 0; + private double treb = 0; + + private int r = 0, g = 0, b = 0; + + // private List colorCache = new ArrayList(); + private double[] cachedBMT = {0f, 0f, 0f}; + + private static Program[] availableActivePrograms = { + Program.ACTIVE_SOLID, + Program.ACTIVE_PULSE, + Program.ACTIVE_FADE, + Program.ACTIVE_SPIN, + Program.ACTIVE_CHASE1, + Program.ACTIVE_CHASE2, + Program.ACTIVE_EXPLODE, + Program.ACTIVE_DIAMOND, + Program.ACTIVE_FROMPT, + Program.ACTIVE_FROMPT2}; + private static Program[] availableFadePrograms = {Program.PASSIVE_DFADE, Program.PASSIVE_HFADE, Program.PASSIVE_FLATFADE, Program.PASSIVE_FMOSAIC}; + private static Program[] availableResponsivePrograms = {Program.RESPONSE_ACROSS, Program.RESPONSE_ALONG}; + + + public ColorPrograms() { + } + + public String getProgramName() { + return currentProgram.name(); + } + + public Color getColorFor(Light light) { + // /*!DEBUG*/currentProgram =RESPONSE_ACROSS;/*!DEBUG*/ + Color c = Color.black; + if (GUI.lightMode == GUI.LIGHT_MODE_FADE) + c = program_DiagonalFades(light, 20000); + else if (GUI.lightMode == GUI.LIGHT_MODE_SOLID) + c = GUI.colors.getStaticColor(); + else if (GUI.lightMode == GUI.LIGHT_MODE_WHITE) + c = Color.white; + else if (GUI.lightMode == GUI.LIGHT_MODE_OFF) + c = Color.black; + else if (GUI.lightMode == GUI.LIGHT_MODE_MOSAIC) + c = (program_Mosaic(light)); + else if (GUI.lightMode == GUI.LIGHT_MODE_CHANNEL) + c = program_Channel(light); + else if (GUI.lightMode == GUI.LIGHT_MODE_AUTO) { + + switch (currentProgram) { + case ACTIVE_SOLID: + c = program_ActiveSolid(light); + break; + case ACTIVE_PULSE: + c = program_ActivePulse(light); + break; + case ACTIVE_FADE: + c = program_ActiveFade(light); + break; + case ACTIVE_SPIN: + c = program_ActiveSpin(light); + break; + case ACTIVE_CHASE1: + c = program_ActiveChase1(light); + break; + case ACTIVE_CHASE2: + c = program_ActiveChase2(light); + break; + case ACTIVE_EXPLODE: + c = program_ActiveExplode(light); + break; + case ACTIVE_DIAMOND: + c = program_ActiveDiamond(light); + break; + case ACTIVE_FROMPT: + c = program_ActiveFromPoint(light); + break; + case ACTIVE_FROMPT2: + c = program_ActiveFromPoint2(light); + break; + + case PASSIVE_DFADE: + c = program_DiagonalFades(light, 5000); + break; + case PASSIVE_HFADE: + c = program_HorizontalFades(light, 5000); + break; + case PASSIVE_FLATFADE: + c = program_FlatFades(light, 5000); + break; + case PASSIVE_FMOSAIC: + c = program_MosaicFast(light); + break; + + case RESPONSE_ACROSS: + c = program_ResponsiveAcross(light); + break; + case RESPONSE_ALONG: + c = program_ResponsiveAlong(light); + break; + } + + } + + return c; + } + + public Color getStaticColor() { + return new Color(r / 255f, g / 255f, b / 255f); + } + + public void updateParameters(int beat, double phase, double bass, double mid, double treb) { + lastBeat = this.beat; + this.beat = beat; + this.phase = phase; + this.bass = bass; + this.mid = mid; + this.treb = treb; + + boolean isBreakdown = BPMDetect.isBreakdown(); + + if ((lastBeat - 16) > this.beat) + changeProgram(isBreakdown); + if (wasBreakdown != isBreakdown && this.beat % 4 == 0) { + changeProgram(isBreakdown); + wasBreakdown = isBreakdown; + } + } + + public void changeProgram(boolean breakdown) { + colorScheme.generateColorScheme(); + if (specificProgram) + return; + boolean responsive = (Math.random() < 0.35); + if (breakdown) { + if (responsive) + currentProgram = availableResponsivePrograms[(int) (Math.random() * availableResponsivePrograms.length)]; + else + currentProgram = availableFadePrograms[(int) (Math.random() * availableFadePrograms.length)]; + } else { + if (responsive) + currentProgram = availableResponsivePrograms[(int) (Math.random() * availableResponsivePrograms.length)]; + else + currentProgram = availableActivePrograms[(int) (Math.random() * availableActivePrograms.length)]; + } + } + + public void changeToProgram(boolean specified, Program program) { + if (specified) { + currentProgram = program; + specificProgram = true; + } else { + specificProgram = false; + } + } + + public void updateStaticColors(int r, int g, int b) { + // please someone explain why i have to flip g and b for this to be right + this.r = r; + this.g = g; + this.b = b; + } + + private Color program_ActiveSolid(Light light) { + return shiftColor(colorScheme.getPrimaryColor(), beat / 6f, 0, 0); + } + + private Color program_ActivePulse(Light light) { + return shiftColor(colorScheme.getPrimaryColor(), beat / 6f, 0, (float) (-phase)); + } + + private Color program_ActiveFade(Light light) { + return shiftColor(colorScheme.getPrimaryColor(), (beat / 6f) + ((float) Math.max(0, (phase - 0.66) * 3) / 6f), 0, 0); + } + + private Color program_ActiveSpin(Light light) { + return shiftColor(colorScheme.getPrimaryColor(), (beat / 4f) + (phase / 8f) + (((light.getUniqueID() + 4) % 8) + 4) % 8 / 16f, 0, 0); + // return Color.black; + } + + private Color program_ActiveChase1(Light light) { + if (Math.abs(light.getUniqueID() - ((int) ((beat + phase) * 8) % 16)) < 2) + return shiftColor(colorScheme.getPrimaryColor(), beat / 6f, 0, 0); + else + return shiftColor(colorScheme.getSecondaryColor(), beat / 6f, 0, -.8f); + } + + private Color program_ActiveChase2(Light light) { + if (Math.abs(light.getUniqueID() - ((int) ((beat + phase) * 8) % 16)) < 2) + return shiftColor(colorScheme.getPrimaryColor(), beat / 6f, 0, 0); + else + return Color.black; + } + + private Color program_ActiveDiamond(Light light) { + Color c = shiftColor(colorScheme.getPrimaryColor(), beat / 6f, 0, 0); + if (Math.abs(light.getX()) > 0.4) // outside + return shiftColor(c, -1f / 6f, 0, 0); + else if ((Math.abs(light.getX()) < 0.4) && (Math.abs(light.getY()) < 0.5)) // inside + return shiftColor(c, 0, 0, 0); + // else if (Math.abs(light.getX()) < 0.4 && Math.abs(light.getY()) >= 0.5) //top/bot + // return shiftColor(c, 0, 0, (phase > speed) ? 0:-1); + + return Color.black; + } + + private Color program_ActiveExplode(Light light) { + Color c = shiftColor(colorScheme.getPrimaryColor(), beat / 6f, 0, 0); + final float speed = 0.15f; + + if (Math.abs(light.getX()) > 0.4) + return shiftColor(c, 0, 0, (phase >= (speed * 2)) ? 0 : -1); + else if ((Math.abs(light.getX()) < 0.4) && (Math.abs(light.getY()) < 0.5)) + return shiftColor(c, 0, 0, (phase < speed) ? 0 : -1); + else if ((Math.abs(light.getX()) < 0.4) && (Math.abs(light.getY()) >= 0.5)) + return shiftColor(c, 0, 0, ((phase < (speed * 2)) && (phase >= speed)) ? 0 : -1); + + return Color.black; + } + + private Color program_ActiveFromPoint(Light light) { + double theta = ((beat * 57.32) - Math.floor(beat * 42.89)) * Math.PI * 2; + Point2D p = new Point2D(Math.sin(theta), Math.cos(theta)); + Point2D l = new Point2D(light.getX(), light.getY()); + + return shiftColor(colorScheme.getPrimaryColor(), beat * .55, 0, -0.5 + 2 * ((phase * 2) - p.distance(l))); + } + + private Color program_ActiveFromPoint2(Light light) { + double theta = (beat * 52.841 + 12.8) % 41.91; + Point2D p = new Point2D(Math.sin(theta), Math.cos(theta)); + Point2D l = new Point2D(light.getX(), light.getY()); + + Color newColor = shiftColor(colorScheme.getPrimaryColor(), (beat + 1) * .55, 0, 1); + Color oldColor = shiftColor(colorScheme.getPrimaryColor(), (beat) * .55, 0, 1 - phase); + + + return averageColors(oldColor, newColor, (p.distance(l) - phase * p.distance(l)) * 2 - 1); + } + + private Color program_DiagonalFades(Light light, int speed) { + // in this case a higher speed value = slower lights + float time = (System.currentTimeMillis() % speed) / (float) speed; + return new Color(Color.HSBtoRGB(((float) (light.getX() + light.getY()) / 8f) + time, .5f, 1f)); + } + + private Color program_HorizontalFades(Light light, int speed) { + // in this case a higher speed value = slower lights + float time = (System.currentTimeMillis() % speed) / (float) speed; + return new Color(Color.HSBtoRGB(((float) (light.getX()) / 4f) + time, .5f, 1f)); + } + + private Color program_FlatFades(Light light, int speed) { + // in this case a higher speed value = slower lights + float time = (System.currentTimeMillis() % speed) / (float) speed; + return new Color(Color.HSBtoRGB(time, .5f, 1f)); + } + + private Color program_ResponsiveAcross(Light light) { + if (light.getUniqueID() == 0) { + if (cachedBMT[0] > bass) + bass = Math.max(cachedBMT[0] - 0.1, bass); + cachedBMT[0] = bass; + if (cachedBMT[1] > mid) + mid = Math.max(cachedBMT[1] - 0.1, mid); + cachedBMT[1] = mid; + if (cachedBMT[2] > treb) + treb = Math.max(cachedBMT[2] - 0.1, treb); + cachedBMT[2] = treb; + } + + if (Math.abs(light.getX()) > 0.4) + return shiftColor(colorScheme.getPrimaryColor(), 0, 0, (float) (bass - 1)); + else if ((Math.abs(light.getX()) < 0.4) && (Math.abs(light.getY()) < 0.5)) + return shiftColor(colorScheme.getSecondaryColor(), 0, 0, (float) (mid - 1)); + else if ((Math.abs(light.getX()) < 0.4) && (Math.abs(light.getY()) >= 0.5)) + return shiftColor(colorScheme.getTertiaryColor(), 0, 0, (float) (treb - 1)); + + // failsafe + return Color.black; + } + + private Color program_ResponsiveAlong(Light light) { + if (light.getUniqueID() == 0) { + if (cachedBMT[0] > bass) + bass = Math.max(cachedBMT[0] - 0.03, bass); + cachedBMT[0] = bass; + if (cachedBMT[1] > mid) + mid = Math.max(cachedBMT[1] - 0.02, mid); + cachedBMT[1] = mid; + if (cachedBMT[2] > treb) + treb = Math.max(cachedBMT[2] - 0.02, treb); + cachedBMT[2] = treb; + } + + if (light.getIndex() < 4) { + return shiftColor(colorScheme.getPrimaryColor(), 0, 0, -1 + Math.max(0, ((float) bass * 4) - light.getIndex())); + } + if ((light.getUniqueID() >= 3) && (light.getUniqueID() < 8)) { + return shiftColor(colorScheme.getSecondaryColor(), 0, 0, -1 + Math.max(0, (((float) mid * 4) - 8) + light.getUniqueID())); + } + if ((light.getUniqueID() >= 12) && (light.getUniqueID() < 16)) { + return shiftColor(colorScheme.getTertiaryColor(), 0, 0, -1 + Math.max(0, (((float) treb * 4) - 16) + light.getUniqueID())); + } + + return Color.black; + } + + private Color program_Channel(Light light) { + if (GUI.channel == light.portRed) + return Color.red; + if (GUI.channel == light.portGreen) + return Color.green; + if (GUI.channel == light.portBlue) + return Color.blue; + return Color.black; + } + + private Color program_Mosaic(Light light) { + float h = (float) Math.sin(((light.getUniqueID() + 1) * System.nanoTime()) / 500000000000d); + return scaleRGB(new Color(Color.HSBtoRGB(h, .5f, 1))); + } + + private Color program_MosaicFast(Light light) { + float h = (float) Math.sin(((light.getUniqueID() % 3.21 + 1) * System.nanoTime()) / 10000000000d); + return new Color(Color.HSBtoRGB(h, .5f, 1)); + } + + private Color shiftColor(Color color, double h, double s, double b) { + return shiftColor(color, (float) h, (float) s, (float) b); + } + + private Color shiftColor(Color color, float h, float s, float b) { + float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null); + hsb[0] += h; + hsb[1] = Math.min(Math.max(hsb[1] + s, 0f), 1f); + hsb[2] = Math.min(Math.max(hsb[2] + b, 0f), 1f); + return new Color(Color.HSBtoRGB(hsb[0], hsb[1], hsb[2])); + } + + private Color averageColors(Color c1, Color c2, double mix) { + float[] rgb = new float[3]; + float[] rgb1 = new float[3]; + float[] rgb2 = new float[3]; + c1.getColorComponents(rgb1); + c2.getColorComponents(rgb2); + rgb[0] = (float) Math.min(Math.max((rgb1[0] * mix + rgb2[0] * (1 - mix)), 0), 1); + rgb[1] = (float) Math.min(Math.max((rgb1[1] * mix + rgb2[1] * (1 - mix)), 0), 1); + rgb[2] = (float) Math.min(Math.max((rgb1[2] * mix + rgb2[2] * (1 - mix)), 0), 1); + return new Color(rgb[0], rgb[1], rgb[2]); + } + + private Color averageColorsGeometric(Color c1, Color c2, double mix) { + float[] rgb = new float[3]; + float[] rgb1 = new float[3]; + float[] rgb2 = new float[3]; + c1.getColorComponents(rgb1); + c2.getColorComponents(rgb2); + rgb[0] = (float) Math.sqrt(rgb1[0] * mix * rgb2[0] * (1 - mix)); + rgb[1] = (float) Math.sqrt(rgb1[1] * mix * rgb2[1] * (1 - mix)); + rgb[2] = (float) Math.sqrt(rgb1[2] * mix * rgb2[2] * (1 - mix)); + return new Color(rgb[0], rgb[1], rgb[2]); + } + + private Color scaleRGB(Color color) { + float[] rgb = new float[3]; + color.getColorComponents(rgb); + rgb[0] = (rgb[0] * r) / 255f; + rgb[1] = (rgb[1] * g) / 255f; + rgb[2] = (rgb[2] * b) / 255f; + return new Color(rgb[0], rgb[1], rgb[2]); + } + +} diff --git a/leigh-audio-bpm/src/main/java/leigh/audio/bpm/ColorScheme.java b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/ColorScheme.java new file mode 100644 index 000000000..39b58b801 --- /dev/null +++ b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/ColorScheme.java @@ -0,0 +1,49 @@ +package leigh.audio.bpm; + +import java.awt.*; + + +public class ColorScheme { + + private static Color[] PRIMARY_COLORS = {Color.red, Color.orange, Color.yellow, Color.green, + Color.cyan, Color.blue, Color.getHSBColor(270, 1f, 1f), + Color.magenta}; + private Color primary = Color.WHITE; + private Color secondary = Color.BLUE; + private Color tertiary = Color.BLACK; + + public ColorScheme() { + generateColorScheme(); + } + + public Color getPrimaryColor() { + return primary; + } + + public Color getSecondaryColor() { + return secondary; + } + + public Color getTertiaryColor() { + return tertiary; + } + + public void generateColorScheme() { + int primaryIndex = (int) (Math.random() * PRIMARY_COLORS.length); + int secondaryIndex = (int) (Math.random() * PRIMARY_COLORS.length); + ; + while (primaryIndex == secondaryIndex) // prevent duplicate colors + secondaryIndex = (int) (Math.random() * PRIMARY_COLORS.length); + if (Math.random() > 0.5) + tertiary = Color.BLACK; + else { + int tertiaryIndex = (int) (Math.random() * PRIMARY_COLORS.length); + while (tertiaryIndex == primaryIndex || tertiaryIndex == secondaryIndex) // prevent duplicate colors + tertiaryIndex = (int) (Math.random() * PRIMARY_COLORS.length); + tertiary = PRIMARY_COLORS[tertiaryIndex]; + } + primary = PRIMARY_COLORS[primaryIndex]; + secondary = PRIMARY_COLORS[secondaryIndex]; + } + +} diff --git a/leigh-audio-bpm/src/main/java/leigh/audio/bpm/ColoredScrollbar.java b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/ColoredScrollbar.java new file mode 100644 index 000000000..7014fdef5 --- /dev/null +++ b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/ColoredScrollbar.java @@ -0,0 +1,83 @@ +package leigh.audio.bpm; + +import javax.swing.*; +import javax.swing.plaf.basic.BasicScrollBarUI; +import java.awt.*; + +public class ColoredScrollbar extends BasicScrollBarUI { + + Color color = Color.WHITE; + boolean inverted = false; + + private JButton b = new JButton() { + // to get rid of warnings + private static final long serialVersionUID = 1L; + + @Override + public Dimension getPreferredSize() { + return new Dimension(0, 0); + } + + }; + + public ColoredScrollbar() { + super(); + } + + public ColoredScrollbar(Color color, boolean inverted) { + super(); + this.color = color; + this.inverted = inverted; + } + + + public Color getColor() { + return color; + } + + public void setColor(Color color) { + this.color = color; + } + + @Override + protected void paintTrack(Graphics g, JComponent c, Rectangle r) { + if (scrollbar.isEnabled()) + g.setColor(color.darker().darker().darker().darker()); + else + g.setColor(color.darker().darker().darker().darker().darker().darker().darker()); + g.fillRect(r.x, r.y, r.width, r.height); + } + + @Override + protected void paintThumb(Graphics g, JComponent c, Rectangle r) { + double pos = (double) scrollbar.getValue() / (double) scrollbar.getMaximum(); + int red, grn, blu; + if (!inverted) { + red = (int) (pos * color.getRed() + (1 - pos) * Color.GRAY.getRed()); + grn = (int) (pos * color.getGreen() + (1 - pos) * Color.GRAY.getGreen()); + blu = (int) (pos * color.getBlue() + (1 - pos) * Color.GRAY.getBlue()); + } else { + red = (int) ((1 - pos) * color.getRed() + pos * Color.GRAY.getRed()); + grn = (int) ((1 - pos) * color.getGreen() + pos * Color.GRAY.getGreen()); + blu = (int) ((1 - pos) * color.getBlue() + pos * Color.GRAY.getBlue()); + } + g.setColor(new Color(red, grn, blu)); + g.fillRoundRect(r.x, r.y, r.width - 1, r.height - 1, 5, 5); + if (scrollbar.isEnabled()) + g.setColor(Color.WHITE); + else + g.setColor(Color.BLACK); + g.drawRoundRect(r.x, r.y, r.width - 1, r.height - 1, 5, 5); + } + + @Override + protected JButton createDecreaseButton(int orientation) { + return b; + } + + @Override + protected JButton createIncreaseButton(int orientation) { + return b; + } + +} diff --git a/leigh-audio-bpm/src/main/java/leigh/audio/bpm/FFTer.java b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/FFTer.java new file mode 100644 index 000000000..99946d471 --- /dev/null +++ b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/FFTer.java @@ -0,0 +1,89 @@ +package leigh.audio.bpm; + +import org.jtransforms.fft.FloatFFT_1D; + + +public class FFTer implements Runnable { // Does FFTs + + FloatFFT_1D fft; + float[] data; + SampleHistory sh; + long lastRan = System.nanoTime(); + + final private int bassStart = 1; // how many fft buckets we want to sum + final private int bassEnd = 6; + final private int midStart = 20; + final private int midEnd = 200; + final private int trebleStart = 700; + final private int trebleEnd = 900; + + private final double avgWeightStrong = 0.001; + private final double avgWeightWeak = avgWeightStrong / 8; + + public static double avgBassLow = 0; + public static double avgBassHigh = 0; + public static double avgMidLow = 0; + public static double avgMidHigh = 0; + public static double avgTrebLow = 0; + public static double avgTrebHigh = 0; + + public FFTer(FloatFFT_1D fft, float[] data, SampleHistory sh) { + this.fft = fft; + this.data = data.clone(); + this.sh = sh; + } + + @Override + public void run() { + // System.out.println("last ran " + (System.nanoTime() - lastRan) + " ago"); + // lastRan = System.nanoTime(); + fft.realForward(data); + float bassVal = 0, midVal = 0, trebleVal = 0; + for (int i = bassStart; i < bassEnd; i++) + bassVal += Math.abs(data[i]); + for (int i = midStart; i < midEnd; i++) + midVal += Math.abs(data[i]); + for (int i = trebleStart; i < trebleEnd; i++) + trebleVal += Math.abs(data[i]); + + bassVal /= bassEnd - bassStart + 100; + midVal /= midEnd - midStart; + trebleVal /= trebleEnd - trebleStart; + midVal /= 4; + + if (bassVal > avgBassLow) + avgBassLow = (1 - avgWeightWeak) * avgBassLow + (avgWeightWeak) * bassVal; + else + avgBassLow = (1 - avgWeightStrong) * avgBassLow + (avgWeightStrong) * bassVal; + + if (bassVal < avgBassHigh) + avgBassHigh = (1 - avgWeightWeak) * avgBassHigh + (avgWeightWeak) * bassVal; + else + avgBassHigh = (1 - avgWeightStrong) * avgBassHigh + (avgWeightStrong) * bassVal; + + + if (midVal > avgMidLow) + avgMidLow = (1 - avgWeightWeak) * avgMidLow + (avgWeightWeak) * midVal; + else + avgMidLow = (1 - avgWeightStrong) * avgMidLow + (avgWeightStrong) * midVal; + + if (midVal < avgMidHigh) + avgMidHigh = (1 - avgWeightWeak) * avgMidHigh + (avgWeightWeak) * midVal; + else + avgMidHigh = (1 - avgWeightStrong) * avgMidHigh + (avgWeightStrong) * midVal; + + + if (trebleVal > avgTrebLow) + avgTrebLow = (1 - avgWeightWeak) * avgTrebLow + (avgWeightWeak) * trebleVal; + else + avgTrebLow = (1 - avgWeightStrong) * avgTrebLow + (avgWeightStrong) * trebleVal; + + if (trebleVal < avgTrebHigh) + avgTrebHigh = (1 - avgWeightWeak) * avgTrebHigh + (avgWeightWeak) * trebleVal; + else + avgTrebHigh = (1 - avgWeightStrong) * avgTrebHigh + (avgWeightStrong) * trebleVal; + + sh.addFftSample(bassVal, midVal, trebleVal); + } + +} diff --git a/leigh-audio-bpm/src/main/java/leigh/audio/bpm/Light.java b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/Light.java new file mode 100644 index 000000000..239f3fa18 --- /dev/null +++ b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/Light.java @@ -0,0 +1,133 @@ +package leigh.audio.bpm; + +public class Light { + + private int uniqueID = 0; + private int index = 0; + private double x = 0, y = 0; + private double r = 0, theta = 0; + private int next = 0, prev = 0; + private double debug_startX = 0, debug_endX = 0, debug_startY = 0, debug_endY = 0; + public int portRed = 0, portBlue = 0, portGreen = 0; + + public double getDebug_endX() { + return debug_endX; + } + + public void setDebug_endX(double debug_endX) { + this.debug_endX = debug_endX; + } + + public double getDebug_startY() { + return debug_startY; + } + + public void setDebug_startY(double debug_startY) { + this.debug_startY = debug_startY; + } + + public double getDebug_endY() { + return debug_endY; + } + + public void setDebug_endY(double debug_endY) { + this.debug_endY = debug_endY; + } + + public Light() { + + } + + public Light(int uniqueID, int index, double sx, double ex, double sy, double ey, int next, int prev) { + this.setUniqueID(uniqueID); + this.setIndex(index); + this.setDebug_startX(sx); + this.setDebug_endX(ex); + this.setDebug_startY(sy); + this.setDebug_endY(ey); + this.setX((sx + ex) / 2d); + this.setY((sy + ey) / 2d); + this.setTheta(Math.atan(y / x)); + this.setR(Math.sqrt(x * x + y * y)); + this.setNext(next); + this.setPrev(prev); + } + + public void setPorts(int r, int g, int b) { + this.portRed = r; + this.portGreen = g; + this.portBlue = b; + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + public double getR() { + return r; + } + + public void setR(double r) { + this.r = r; + } + + public double getTheta() { + return theta; + } + + public void setTheta(double theta) { + this.theta = theta; + } + + public int getNext() { + return next; + } + + public void setNext(int next) { + this.next = next; + } + + public int getPrev() { + return prev; + } + + public void setPrev(int prev) { + this.prev = prev; + } + + public int getUniqueID() { + return uniqueID; + } + + public void setUniqueID(int uniqueID) { + this.uniqueID = uniqueID; + } + + public double getDebug_startX() { + return debug_startX; + } + + public void setDebug_startX(double debug_startX) { + this.debug_startX = debug_startX; + } +} diff --git a/leigh-audio-bpm/src/main/java/leigh/audio/bpm/LightCollection.java b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/LightCollection.java new file mode 100644 index 000000000..3aea32b18 --- /dev/null +++ b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/LightCollection.java @@ -0,0 +1,100 @@ +package leigh.audio.bpm; + +import java.util.ArrayList; + + +public class LightCollection extends ArrayList { + + private static final long serialVersionUID = 1L; + + public LightCollection() { + + } + + public Light addLight(int uniqueID, int index, double sx, double ex, double sy, double ey, int next, int prev) { + Light light = new Light(uniqueID, index, sx, ex, sy, ey, next, prev); + this.add(light); + return light; + } + + public boolean removeLightByUID(int uniqueID) { + for (int i = 0; i < this.size(); i++) { + if (this.get(i).getUniqueID() == uniqueID) { + this.remove(i); + return true; + } + } + return false; + } + + public Light getLightByUID(int uniqueID) { + for (int i = 0; i < this.size(); i++) { + if (this.get(i).getUniqueID() == uniqueID) { + return this.get(i); + } + } + return null; + } + + public static LightCollection initializeLivingRoomLights() { + /** DIAGRAM + + / \ | / \ y=1 + 1/ 2\ | /10 \9 + | + / / | \ \ + 0/ 11/ | \3 \8 + --------------+---------------- 0 + \ \ | / / + 15\ 4\ | /12 /7 + | + \ / | \ / + 14\ /13| 5\ /6 -1 + + x= -1 0 1 + UID is listed next to light. + Index is UID for 0 to 7, or (UID - 8) for 8 to 15. + + */ + LightCollection lights = new LightCollection(); + + lights.addLight(0, 0, -1, -2d / 3d, 0, .5d, 1, 15); + lights.addLight(1, 1, -2d / 3d, -1d / 3d, .5d, 1, 2, 0); + lights.addLight(2, 2, -1d / 3d, 0, 1, .5d, 3, 1); + lights.addLight(3, 3, 0, 1d / 3d, .5d, 0, 4, 2); + lights.addLight(4, 4, -1d / 3d, 0, 0, -.5d, 5, 3); + lights.addLight(5, 5, 0, 1 / 3d, -.5d, -1, 6, 4); + lights.addLight(6, 6, 1d / 3d, 2d / 3d, -1, -.5d, 7, 5); + lights.addLight(7, 7, 2 / 3d, 1, -.5d, 0, 8, 6); + + lights.addLight(8, 0, 1, 2d / 3d, 0, .5d, 9, 7); + lights.addLight(9, 1, 2d / 3d, 1d / 3d, .5d, 1, 2, 0); + lights.addLight(10, 2, 1d / 3d, 0, 1, .5d, 3, 1); + lights.addLight(11, 3, 0, -1d / 3d, .5d, 0, 4, 2); + lights.addLight(12, 4, 1d / 3d, 0, 0, -.5d, 5, 3); + lights.addLight(13, 5, 0, -1 / 3d, -.5d, -1, 6, 4); + lights.addLight(14, 6, -1d / 3d, -2d / 3d, -1, -.5d, 7, 5); + lights.addLight(15, 7, -2 / 3d, -1, -.5d, 0, 8, 6); + + lights.get(0).setPorts(99, 99, 99); + lights.get(1).setPorts(39, 40, 38); + lights.get(2).setPorts(42, 43, 41); + lights.get(3).setPorts(52, 53, 51); + lights.get(4).setPorts(4, 5, 3); + lights.get(5).setPorts(26, 27, 25); + lights.get(6).setPorts(23, 24, 22); + lights.get(7).setPorts(99, 99, 99); + + lights.get(8).setPorts(99, 99, 99); + lights.get(9).setPorts(36, 37, 35); + lights.get(10).setPorts(58, 59, 57); + lights.get(11).setPorts(33, 34, 32); + lights.get(12).setPorts(10, 11, 9); + lights.get(13).setPorts(7, 8, 6); + lights.get(14).setPorts(1, 2, 0); + lights.get(15).setPorts(99, 99, 99); + + + return lights; + } +} diff --git a/leigh-audio-bpm/src/main/java/leigh/audio/bpm/LightMessage.java b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/LightMessage.java new file mode 100644 index 000000000..0e3bad4df --- /dev/null +++ b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/LightMessage.java @@ -0,0 +1,12 @@ +package leigh.audio.bpm; + +public class LightMessage { + + public int channel = 0; + public int value = 0; + + public LightMessage(int channel, int value) { + this.channel = channel; + this.value = value; + } +} \ No newline at end of file diff --git a/leigh-audio-bpm/src/main/java/leigh/audio/bpm/Pair.java b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/Pair.java new file mode 100644 index 000000000..cf43a9a04 --- /dev/null +++ b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/Pair.java @@ -0,0 +1,11 @@ +package leigh.audio.bpm; + +public class Pair { + public final X x; + public final Y y; + + public Pair(X x, Y y) { + this.x = x; + this.y = y; + } +} \ No newline at end of file diff --git a/leigh-audio-bpm/src/main/java/leigh/audio/bpm/SampleHistory.java b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/SampleHistory.java new file mode 100644 index 000000000..86454e6f5 --- /dev/null +++ b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/SampleHistory.java @@ -0,0 +1,85 @@ +package leigh.audio.bpm; + +public class SampleHistory { + + + public static final int FFT_BASS = 1; + public static final int FFT_MID = 2; + public static final int FFT_TREBLE = 3; + +// private final float BPM_MIN = 79.5F; +// private final float BPM_MAX = 159F; + + + private int fftSampleArraySize; // 10 seconds + private float fftBassSamples[]; + private float fftMidSamples[]; + private float fftTrebleSamples[]; + private float samples[]; + + public SampleHistory(int sampleRate, int historySeconds) { + fftSampleArraySize = sampleRate * historySeconds; + fftBassSamples = new float[fftSampleArraySize]; + fftMidSamples = new float[fftSampleArraySize]; + fftTrebleSamples = new float[fftSampleArraySize]; + samples = new float[1024]; + } + + + public void addFftSample(float fftBass, float fftMid, float fftTreble) { + for (int i = 0; i < fftSampleArraySize - 1; i++) { + fftBassSamples[i] = fftBassSamples[i + 1]; + fftMidSamples[i] = fftMidSamples[i + 1]; + fftTrebleSamples[i] = fftTrebleSamples[i + 1]; + } + fftBassSamples[fftSampleArraySize - 1] = fftBass; + fftMidSamples[fftSampleArraySize - 1] = fftMid; + fftTrebleSamples[fftSampleArraySize - 1] = fftTreble; + + } + + public float[] getFftSamples(int type) { + switch (type) { + case FFT_BASS: + return fftBassSamples.clone(); + case FFT_MID: + return fftMidSamples.clone(); + case FFT_TREBLE: + return fftTrebleSamples.clone(); + default: + return null; + } + } + + + public float[] getSamples() { + final float[] s = samples.clone(); + return s; + } + + + public float getMaxInArray() { + return getMaxInArray(fftBassSamples); + } + + + public static float getMaxInArray(float[] array) { + float max = 0; + for (int i = 0; i < array.length; i++) + if (array[i] > max) + max = array[i]; + return max; + } + + public static float getMaxInArrayDecay(float[] array) { + float max = 0; + for (int i = 0; i < array.length; i++) + if (i < array.length * 3 / 4) + if (array[i] * 0.8f + (0.2f * i / array.length) > max) + max = array[i]; + else if (array[i] * 0.2f + (0.75f * i / array.length) > max) + max = array[i]; + return max * 1.5f; + } + +} diff --git a/leigh-audio-bpm/src/main/java/leigh/audio/bpm/SerialCache.java b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/SerialCache.java new file mode 100644 index 000000000..5abf3f429 --- /dev/null +++ b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/SerialCache.java @@ -0,0 +1,70 @@ +package leigh.audio.bpm; + +import java.util.concurrent.LinkedBlockingQueue; + +public class SerialCache implements Runnable { + + + private int[] values = new int[512]; + private SerialWriter writer; + + private LinkedBlockingQueue queue = new LinkedBlockingQueue(200); + + public SerialCache(SerialWriter writer) { + this.writer = writer; + } + + public void addMessage(int port, int value) { + queue.add(new LightMessage(port, value)); + } + + public void WriteData(int rPort, int gPort, int bPort, int rData, int gData, int bData) { + if (rPort > 4095) + rPort = 4095; + if (gPort > 4095) + gPort = 4095; + if (bPort > 4095) + bPort = 4095; + + if (values[rPort] == rData && values[gPort] == gData && values[bPort] == bData) + return; + values[rPort] = rData; + values[gPort] = gData; + values[bPort] = bData; + + // writer.writeData(SerialWriter.stringFromRGBData(rPort, gPort, bPort, rData, gData, bData)); + writer.writeData2(rPort, gPort, bPort, rData, gData, bData); + } + + /* + * (non-Javadoc) + * @see java.lang.Runnable#run() + */ + @Override + public void run() { + while (true) { + if (queue.isEmpty()) { + try { + Thread.yield(); + } catch (Exception e) { + e.printStackTrace(); + } + } else { + try { + LightMessage msg = queue.poll(); + writer.writeData2(msg.channel, msg.value); + } catch (Exception e) { + e.printStackTrace(); + } + // System.out.println("[SerialCache] run() " + msg.channel + ", " + msg.value); + //queue.remove(); + } + //if (queue.) + } + } + + public void clearQueue() { + queue.clear(); + } + +} diff --git a/leigh-audio-bpm/src/main/java/leigh/audio/bpm/SerialWriter.java b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/SerialWriter.java new file mode 100644 index 000000000..10e389679 --- /dev/null +++ b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/SerialWriter.java @@ -0,0 +1,159 @@ +package leigh.audio.bpm; + +import gnu.io.CommPortIdentifier; +import gnu.io.SerialPort; +import gnu.io.SerialPortEvent; +import gnu.io.SerialPortEventListener; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.Enumeration; + +public class SerialWriter implements SerialPortEventListener { + + int hue; + // final static double PWM_MULT = 1; + boolean errorWritten = false; + + SerialPort serialPort; + /** + * The port we're normally going to use. + */ + private static final String PORT_NAMES[] = {"/dev/tty.usbserial-A9007UX1", // Mac OS X + "/dev/ttyUSB0", // Linux + GUI.port, // Windows + }; + /** + * A BufferedReader which will be fed by a InputStreamReader converting the bytes into characters making the + * displayed results codepage independent + */ + private BufferedReader input; + /** + * The output stream to the port + */ + private OutputStream output; + /** + * Milliseconds to block while waiting for port open + */ + private static final int TIME_OUT = 2000; + /** + * Default bits per second for COM port. + */ + private static final int DATA_RATE = 128000; + + + public void initialize() { + CommPortIdentifier portId = null; + @SuppressWarnings("rawtypes") + Enumeration portEnum = CommPortIdentifier.getPortIdentifiers(); + + // First, Find an instance of serial port as set in PORT_NAMES. + while (portEnum.hasMoreElements()) { + CommPortIdentifier currPortId = (CommPortIdentifier) portEnum.nextElement(); + for (String portName : PORT_NAMES) { + System.out.println("Found port " + currPortId.getName()); + if (currPortId.getName().equals(portName)) { + portId = currPortId; + break; + } + } + } + if (portId == null) { + System.out.println("Could not find COM port."); + return; + } + + try { + // open serial port, and use class name for the appName. + serialPort = (SerialPort) portId.open(this.getClass().getName(), TIME_OUT); + + // set port parameters + serialPort.setSerialPortParams(DATA_RATE, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, + SerialPort.PARITY_NONE); + + // open the streams + input = new BufferedReader(new InputStreamReader(serialPort.getInputStream())); + output = serialPort.getOutputStream(); + + // add event listeners + serialPort.addEventListener(this); + serialPort.notifyOnDataAvailable(true); + } catch (Exception e) { + System.err.println(e.toString()); + } + } + + public synchronized void writeData(String data) { + try { + // Thread.sleep(1); + output.write(data.getBytes()); + } catch (IOException e) { + System.out.println(e.toString()); + } catch (Exception e) { + if (!errorWritten) { + System.out.println("\nAttempted to open port " + GUI.port + ":"); + e.printStackTrace(); + errorWritten = true; + } + } + } + + public static String stringFromRGBData(int rPort, int gPort, int bPort, int rData, int gData, int bData) { + int red = 4095 - (rData); + int grn = 4095 - (gData); + int blu = 4095 - (bData); + if (red < 0 || red > 4095) + System.out.println("red of " + red); + if (grn < 0 || grn > 4095) + System.out.println("grn of " + grn); + if (blu < 0 || blu > 4095) + System.out.println("grn of " + blu); + return new String(rPort + " " + gPort + " " + bPort + " " + red + " " + grn + " " + blu + " "); + } + + public void writeData2(int rPort, int gPort, int bPort, int rData, int gData, int bData) { + String output = String.format("%04d,%04d,", rPort, rData); + writeData(output); + System.out.println("r" + output); + output = String.format("%04d,%04d,", gPort, gData); + writeData(output); + // System.out.println("g"+output); + // output = String.format("%04d,%04d,", bPort, bData); + // writeData(output); + // System.out.println("b"+output); + } + + public void writeData2(int port, int value) { + // String output = String.format("%04d,%04d,", port, value); + // writeData(output); + // System.out.println("out:"+output); + String output = // String.valueOf(port) + ";" + String.valueOf(value) + ";" + String.valueOf(port) + ";" + String.valueOf(value * GUI.LIGHT_OUTPUT_SCALE) + "\n"; + writeData(output); + // System.out.println(output); + } + + @Override + public synchronized void serialEvent(SerialPortEvent arg0) { + if (arg0.getEventType() == SerialPortEvent.DATA_AVAILABLE) { + try { + String inputLine = input.readLine(); + System.out.println("Serial input: " + inputLine); + } catch (Exception e) { + System.out.println("Serial read error"); + System.err.println(e.toString()); + } + } + + } + + public synchronized void close() { + if (serialPort != null) { + serialPort.removeEventListener(); + serialPort.close(); + } + } + +} \ No newline at end of file diff --git a/leigh-audio-bpm/src/main/java/leigh/audio/bpm/WaveformPanel.java b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/WaveformPanel.java new file mode 100644 index 000000000..936623045 --- /dev/null +++ b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/WaveformPanel.java @@ -0,0 +1,88 @@ +package leigh.audio.bpm; + +import javax.swing.*; +import java.awt.*; + + +public class WaveformPanel extends JPanel { + private static final long serialVersionUID = -182562858491495098L; + + SampleHistory sh; + + public WaveformPanel(SampleHistory sh) { + this.sh = sh; + } + + @Override + public void paint(Graphics g) { + + super.paint(g); + + + float max = sh.getMaxInArray(); + int w = getWidth(); + int h = getHeight(); + + + g.clearRect(0, 0, w, h); + + for (int sampleType = 3; sampleType > 0; sampleType--) { + float[] samples = smoothSamples(sh.getFftSamples(sampleType)); + + Color color = null; + switch (sampleType) { + case SampleHistory.FFT_BASS: + color = Color.RED; + break; + case SampleHistory.FFT_MID: + color = Color.GREEN; + break; + case SampleHistory.FFT_TREBLE: + color = Color.BLUE; + break; + } + g.setColor(color); + + for (int i = 0; i < (samples.length - 1); i++) { + int x1 = w * i / samples.length; + int x2 = w * (i + 1) / samples.length; + int y1, y2; + if (i == 998) { + y1 = h - (int) (samples[i] * h / max); + y2 = h - (int) (samples[i + 1] * h / max); + } else { + // Smoothed waveform + y1 = h - (int) (samples[i] * h / max); + y2 = h - (int) (samples[i + 1] * h / max); + } + g.drawLine(x1, y1, x2, y2); + } + if (sampleType == SampleHistory.FFT_BASS) { + String sBPM = String.valueOf(BPMDetect.detectBPM(samples, GUI.sampleRate, SampleHistory.FFT_BASS)); + g.drawChars(sBPM.toCharArray(), 0, sBPM.length(), 25, 25); + } + + } + +// g.setColor(Color.BLACK); +// int x = w * BPMDetect.detectAt / sh.getFftSamples(SampleHistory.FFT_BASS).length; +// g.drawLine(x, 0, x, w); + + + } + + + public float[] smoothSamples(float[] samples) { + int length = samples.length; + float result[] = new float[length]; + + if (length < 2) + return null; + for (int i = 2; i < samples.length - 1; i++) + result[i] = (samples[i - 1] + samples[i] + samples[i + 1]) / 3; + result[0] = samples[0]; + result[length - 1] = samples[length - 1]; + return result; + } + +} diff --git a/leigh-audio-bpm/src/main/java/leigh/audio/bpm/pgm_ActiveSolid.java b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/pgm_ActiveSolid.java new file mode 100644 index 000000000..50f29a9d0 --- /dev/null +++ b/leigh-audio-bpm/src/main/java/leigh/audio/bpm/pgm_ActiveSolid.java @@ -0,0 +1,42 @@ +package leigh.audio.bpm; + +import java.awt.*; + +/** + * + */ + + +/** @author Widget */ +public class pgm_ActiveSolid extends ColorProgram { + + /* + * (non-Javadoc) + * @see ColorProgram#getColor(Light, ColorScheme) + */ + @Override + public Color getColor(Light light, ColorScheme colorScheme) { + return shiftColor(colorScheme.getPrimaryColor(), beat / 6f, 0, 0); + } + + /* + * (non-Javadoc) + * @see ColorProgram#getProgramName() + */ + @Override + public String getProgramName() { + // TODO Auto-generated method stub + return "Active: Solid"; + } + + /* + * (non-Javadoc) + * @see ColorProgram#getProgramID() + */ + @Override + public int getProgramID() { + // TODO Auto-generated method stub + return 101; + } + +} diff --git a/settings.gradle b/settings.gradle index 3dc443c06..54b79fb52 100644 --- a/settings.gradle +++ b/settings.gradle @@ -93,4 +93,5 @@ include 'leigh-chain-pki' include 'leigh-chain-pki-service' include 'leigh-video' include 'leigh-video-edltool' +include 'leigh-audio-bpm' -- GitLab