diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..32526a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.gradle/* +build/* +bin/* +.vscode/settings.json +samples/Abracadabra.mp3 +samples/edamame.mp3 +Target/*.sm +Target/*.mp3 \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..bda2783 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,58 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "name": "30s preview", + "request": "launch", + "mainClass": "autostepper.AutoStepper", + "projectName": "AutoStepper", + "args": "input=samples/${input:mp3} output=${input:out_dir} duration=30 preview=true", + "vmArgs": "-Xmx4096m", + "console": "integratedTerminal" + }, + { + "type": "java", + "name": "Full", + "request": "launch", + "mainClass": "autostepper.AutoStepper", + "projectName": "AutoStepper", + "args": "input=samples/${input:mp3} output=${input:out_dir} duration=300", + "vmArgs": "-Xmx4096m", + "console": "integratedTerminal" + }, + { + "type": "java", + "name": "PreviewAll", + "request": "launch", + "mainClass": "autostepper.AutoStepper", + "projectName": "AutoStepper", + "args": "input=samples/${input:mp3} output=${input:out_dir} duration=300 preview=true", + "vmArgs": "-Xmx4096m", + "console": "integratedTerminal" + } + ], + "inputs": [ + { + "id": "mp3", + "type": "pickString", + "description": "Select mp3 to use", + "options": ["PopVocal-AgusAlvarez&Markvard-BeautifulLiar(freetouse.com).mp3", + "HipHop-Aylex-GoodDays(freetouse.com).mp3", + "Electro-Aylex-TurnItLouder(freetouse.com).mp3", + "Disco-Burgundy-DreamsofTomorrow(freetouse.com).mp3", + "DontAskWhatTheHeckIsThat.mp3", + "Abracadabra.mp3"], + "default": "Abracadabra.mp3" + }, + { + "id": "out_dir", + "type": "promptString", + "description": "Select output to use", + "default": "out" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index ef49ccb..480104a 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,15 @@ updatesm=true will look for existing .sm stepfiles using the same filenames gene You can also use the output as a base to further edit & perfect songs, with AutoStepper doing most of the dirty work. + I will add it is optimized for pad use, not keyboard use (e.g. difficulty isn't high enough). +### JDK for development +You can get it from adoptium.net, in case you work on Windows + +### Minim library +All credits for processing to author of: https://github.com/ddf/Minim/tree/main + Phr00t ** LICENSING: Modified MIT license to restrict commercial use & require attribution ** @@ -53,3 +60,13 @@ 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. + +## Fork information + +This is a fork of "Phr00t's Software". +Original copyright © 2018 Phr00t's Software. + +Modifications and additional code: +Copyright © 2025 Fightlapa - github + +This fork is distributed under the same license as the original project. diff --git a/Target/parse.py b/Target/parse.py new file mode 100644 index 0000000..e71b12f --- /dev/null +++ b/Target/parse.py @@ -0,0 +1,63 @@ +# To convert sm file written by professionalist +# to compare later on with algorithm there for matching accuracy + +# Open input file +input_filename = "Abracadabra.sm" +output_filename = "AbracadabraOut.sm" + +with open(input_filename, "r") as f: + lines = f.readlines() + +output_lines = [] +current_block = [] + +first_trigger = False +second_trigger = False +parse_mode = False + +for line in lines: + line = line.strip() + if parse_mode: + if line == "," or line == ";": # End of a block + # Process current block: add '.' to each line + index = 0 + for l in current_block: + # print (f"Block fragment: {l} len: {len(current_block)} index: {index % 2 == 0}") + if len(current_block) == 16: + output_lines.append(l) + elif len(current_block) == 4: + output_lines.append(l) + output_lines.append("0000") + output_lines.append("0000") + output_lines.append("0000") + elif len(current_block) == 8: + output_lines.append(l) + output_lines.append("0000") + index+=1 + # You can uncomment it to add also "," lines + # output_lines.append(line) + # for l in output_lines: + # print (f"Output fragment: {l}") + current_block = [] + elif len(line) == 4: # Skip empty lines + current_block.append(line) + elif "dance-single" in line or "dance-double" in line: + first_trigger = False + second_trigger = False + parse_mode = False + break + else: + output_lines.append(line) + else: + if "dance-single:" in line: + first_trigger = True + elif "Hard:" in line: + second_trigger = True + if first_trigger and second_trigger: + parse_mode = True + +# Save to new file +with open(output_filename, "w") as f: + f.write("\n".join(output_lines)) + +print(f"Processed file saved as {output_filename}") diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..3b402af --- /dev/null +++ b/build.gradle @@ -0,0 +1,101 @@ +plugins { + id 'java' + id 'application' + id 'com.github.johnrengelman.shadow' version '7.1.2' +} + +sourceCompatibility = JavaVersion.VERSION_17 +targetCompatibility = JavaVersion.VERSION_17 + +sourceSets { + main { + java { + srcDirs = ['src'] + } + } + test { + resources { + srcDirs = ['test'] + } + } +} + +application { + mainClass = 'autostepper.AutoStepper' +} + +repositories { + mavenCentral() + maven { url = uri('https://repo.clojars.org/') } +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' + + // Argument parsing + implementation 'net.sf.jopt-simple:jopt-simple:5.0.4' + + // HTML parsing + implementation 'org.jsoup:jsoup:1.11.2' + + // Trove collections (gnu.trove.*) + implementation 'net.sf.trove4j:trove4j:3.0.3' + + // MP3 decoder (JLayer) + implementation 'javazoom:jlayer:1.0.1' + + // JavaSound SPI – MP3 + implementation 'com.googlecode.soundlibs:mp3spi:1.9.5-1' + + // Tritonus (JavaSound implementation) + implementation 'com.googlecode.soundlibs:tritonus-share:0.3.7-2' + implementation 'org.clojars.automata:tritonus-aos:1.0.0' + implementation 'org.clojars.automata:jsminim:2.1.0' +} + +test { + useJUnitPlatform() +} + +shadowJar { + archiveBaseName.set('AutoStepper') + archiveClassifier.set('') // replaces normal jar + + mergeServiceFiles() +} + +// Make the distribution use the shadow JAR +tasks.named('distZip') { + dependsOn shadowJar + // Optional: include shadowJar artifact instead of default jar + from(shadowJar.archiveFile) { + into('lib') + } +} + +tasks.named('installDist') { + dependsOn shadowJar + from(shadowJar.archiveFile) { + into('lib') + } +} + +tasks.named('distTar') { + enabled = false +} + +tasks.named('startScripts') { + enabled = false +} + +tasks.named('startShadowScripts') { + enabled = false +} + +tasks.named('shadowDistTar') { + enabled = false +} + +tasks.named('shadowDistZip') { + enabled = false +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..62d4c05 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..59bc51a --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9b30e15 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega \ No newline at end of file diff --git a/lib/export.txt b/lib/export.txt deleted file mode 100644 index f2681ac..0000000 --- a/lib/export.txt +++ /dev/null @@ -1 +0,0 @@ -name = Minim Audio diff --git a/lib/jl1.0.1.jar b/lib/jl1.0.1.jar deleted file mode 100644 index bb6b623..0000000 Binary files a/lib/jl1.0.1.jar and /dev/null differ diff --git a/lib/jopt-simple-5.0.4.jar b/lib/jopt-simple-5.0.4.jar deleted file mode 100644 index 317b2b0..0000000 Binary files a/lib/jopt-simple-5.0.4.jar and /dev/null differ diff --git a/lib/jsminim.jar b/lib/jsminim.jar deleted file mode 100644 index 6eef2b2..0000000 Binary files a/lib/jsminim.jar and /dev/null differ diff --git a/lib/jsoup-1.11.2.jar b/lib/jsoup-1.11.2.jar deleted file mode 100644 index e4be2ae..0000000 Binary files a/lib/jsoup-1.11.2.jar and /dev/null differ diff --git a/lib/mp3spi1.9.5.jar b/lib/mp3spi1.9.5.jar deleted file mode 100644 index 0c74dae..0000000 Binary files a/lib/mp3spi1.9.5.jar and /dev/null differ diff --git a/lib/tritonus_aos.jar b/lib/tritonus_aos.jar deleted file mode 100644 index 4a02386..0000000 Binary files a/lib/tritonus_aos.jar and /dev/null differ diff --git a/lib/tritonus_share.jar b/lib/tritonus_share.jar deleted file mode 100644 index bb367d1..0000000 Binary files a/lib/tritonus_share.jar and /dev/null differ diff --git a/lib/trove-3.0.3.jar b/lib/trove-3.0.3.jar deleted file mode 100644 index cd00f93..0000000 Binary files a/lib/trove-3.0.3.jar and /dev/null differ diff --git a/samples/Cure.mp3 b/samples/Cure.mp3 new file mode 100644 index 0000000..c668d45 Binary files /dev/null and b/samples/Cure.mp3 differ diff --git a/samples/Disco-Burgundy-DreamsofTomorrow(freetouse.com).mp3 b/samples/Disco-Burgundy-DreamsofTomorrow(freetouse.com).mp3 new file mode 100644 index 0000000..bfcfecf Binary files /dev/null and b/samples/Disco-Burgundy-DreamsofTomorrow(freetouse.com).mp3 differ diff --git a/samples/DontAskWhatTheHeckIsThat.mp3 b/samples/DontAskWhatTheHeckIsThat.mp3 new file mode 100644 index 0000000..ee708bf Binary files /dev/null and b/samples/DontAskWhatTheHeckIsThat.mp3 differ diff --git a/samples/Electro-Aylex-TurnItLouder(freetouse.com).mp3 b/samples/Electro-Aylex-TurnItLouder(freetouse.com).mp3 new file mode 100644 index 0000000..99a972a Binary files /dev/null and b/samples/Electro-Aylex-TurnItLouder(freetouse.com).mp3 differ diff --git a/samples/HipHop-Aylex-GoodDays(freetouse.com).mp3 b/samples/HipHop-Aylex-GoodDays(freetouse.com).mp3 new file mode 100644 index 0000000..713961a Binary files /dev/null and b/samples/HipHop-Aylex-GoodDays(freetouse.com).mp3 differ diff --git a/samples/PopVocal-AgusAlvarez&Markvard-BeautifulLiar(freetouse.com).mp3 b/samples/PopVocal-AgusAlvarez&Markvard-BeautifulLiar(freetouse.com).mp3 new file mode 100644 index 0000000..7236b2f Binary files /dev/null and b/samples/PopVocal-AgusAlvarez&Markvard-BeautifulLiar(freetouse.com).mp3 differ diff --git a/src/autostepper/AutoStepper.java b/src/autostepper/AutoStepper.java index 79da9ad..fec918c 100644 --- a/src/autostepper/AutoStepper.java +++ b/src/autostepper/AutoStepper.java @@ -11,93 +11,117 @@ import java.util.ArrayList; import java.util.Scanner; +import autostepper.genetic.AlgorithmParameter; +import autostepper.genetic.GeneticOptimizer; +import autostepper.misc.Averages; +import autostepper.moveassigners.SimfileDifficulty; +import autostepper.smfile.SmFileParser; +import autostepper.soundprocessing.Song; + /** * * @author Phr00t */ + +// TODO: +// ensure volume array is as big as other arrays as those are used together +// Jump has to be higher than tap +// do something with vibe assignment as it can block learning from working properly +// Maybe parametrize vibes +// Then remove sustain as it's not really needed at this point yet... let's focus on just taps and jumps public class AutoStepper { - - public static boolean DEBUG_STEPS = false; - public static float MAX_BPM = 170f, MIN_BPM = 70f, BPM_SENSITIVITY = 0.05f, STARTSYNC = 0.0f; + + public static boolean TRAIN = true; + public static boolean INDICATOR = false; + public static boolean DEBUG_STEPS = true; + public static boolean RANDOMIZED = false; + public static boolean PREVIEW_DETECTION = false; + + public static boolean SHOW_INFO = false; + public static boolean DEBUG_TIMINGS = true; + + // Having let's say sample rate of 44.100 + // We might want to reduce it a bit + + public static float STARTSYNC = 0.0f; + public static double TAPSYNC = -0.11; public static boolean USETAPPER = false, HARDMODE = false, UPDATESM = false; - - public static Minim minim; + + public static Minim minimLib; public static AutoStepper myAS = new AutoStepper(); - - public static final int KICKS = 0, ENERGY = 1, SNARE = 2, HAT = 3; - - // collected song data - private final TFloatArrayList[] manyTimes = new TFloatArrayList[4]; - private final TFloatArrayList[] fewTimes = new TFloatArrayList[4]; - + // for minim - public String sketchPath( String fileName ) { + public String sketchPath(String fileName) { return fileName; } - + // for minim - public InputStream createInput( String fileName ) { + public InputStream createInput(String fileName) { try { return new FileInputStream(new File(fileName)); - } catch(Exception e) { + } catch (Exception e) { return null; } } - + // argument parser public static String getArg(String[] args, String argname, String def) { try { - for(String s : args) { + for (String s : args) { s = s.replace("\"", ""); - if( s.startsWith(argname) ) { + if (s.startsWith(argname)) { return s.substring(s.indexOf("=") + 1).toLowerCase(); } } - } catch(Exception e) { } + } catch (Exception e) { + } return def; } - + // argument parser public static boolean hasArg(String[] args, String argname) { - for(String s : args) { - if( s.toLowerCase().equals(argname) ) return true; + for (String s : args) { + if (s.toLowerCase().equals(argname)) + return true; } return false; } - + public static void main(String[] args) { - minim = new Minim(myAS); + minimLib = new Minim(myAS); String outputDir, input; float duration; - System.out.println("Starting AutoStepper by Phr00t's Software, v1.7 (See www.phr00t.com for more goodies!)"); - if( hasArg(args, "help") || hasArg(args, "h") || hasArg(args, "?") || hasArg(args, "-help") || hasArg(args, "-?") || hasArg(args, "-h") ) { + System.out.println( + "Starting AutoStepper by Phr00t's Software, v1.7 (See www.phr00t.com for more goodies!). Fork by Fightlapa - big thanks to original author."); + if (hasArg(args, "help") || hasArg(args, "h") || hasArg(args, "?") || hasArg(args, "-help") + || hasArg(args, "-?") || hasArg(args, "-h")) { System.out.println("Argument usage (all fields are optional):\n" + "input= output= duration= tap= tapsync= hard= updatesm="); return; } - MAX_BPM = Float.parseFloat(getArg(args, "maxbpm", "170f")); outputDir = getArg(args, "output", "."); - if( outputDir.endsWith("/") == false ) outputDir += "/"; + if (outputDir.endsWith("/") == false) + outputDir += "/"; input = getArg(args, "input", "."); duration = Float.parseFloat(getArg(args, "duration", "90")); STARTSYNC = Float.parseFloat(getArg(args, "synctime", "0.0")); - BPM_SENSITIVITY = Float.parseFloat(getArg(args, "bpmsensitivity", "0.05")); - USETAPPER = getArg(args, "tap", "false").equals("true"); + PREVIEW_DETECTION = getArg(args, "preview", "false").equals("true"); TAPSYNC = Double.parseDouble(getArg(args, "tapsync", "-0.11")); - HARDMODE = getArg(args, "hard", "false").equals("true"); - UPDATESM = getArg(args, "updatesm", "false").equals("true"); File inputFile = new File(input); - if( inputFile.isFile() ) { - myAS.analyzeUsingAudioRecordingStream(inputFile, duration, outputDir); - } else if( inputFile.isDirectory() ) { + + duration = correctTime(inputFile, duration); + + if (inputFile.isFile()) { + myAS.analyzeUsingAudioRecordingStream(inputFile, duration, outputDir); + } else if (inputFile.isDirectory()) { System.out.println("Processing directory: " + inputFile.getAbsolutePath()); File[] allfiles = inputFile.listFiles(); - for(File f : allfiles) { + for (File f : allfiles) { String extCheck = f.getName().toLowerCase(); - if( f.isFile() && - (extCheck.endsWith(".mp3") || extCheck.endsWith(".wav")) ) { - myAS.analyzeUsingAudioRecordingStream(f, duration, outputDir); + if (f.isFile() && + (extCheck.endsWith(".mp3") || extCheck.endsWith(".wav"))) { + myAS.analyzeUsingAudioRecordingStream(f, duration, outputDir); } else { System.out.println("Skipping unsupported file: " + f.getName()); } @@ -107,313 +131,78 @@ public static void main(String[] args) { } } - TFloatArrayList calculateDifferences(TFloatArrayList arr, float timeThreshold) { - TFloatArrayList diff = new TFloatArrayList(); - int currentlyAt = 0; - while(currentlyAt < arr.size() - 1) { - float mytime = arr.getQuick(currentlyAt); - int oldcurrentlyat = currentlyAt; - for(int i=currentlyAt+1;i= timeThreshold ) { - diff.add(diffcheck); - currentlyAt = i; - break; - } - } - if( oldcurrentlyat == currentlyAt ) break; - } - return diff; - } - - float getDifferenceAverage(TFloatArrayList arr) { - float avg = 0f; - for(int i=0;i values = new ArrayList<>(); - for(int i=0;i longest || - check.size() == longest && getDifferenceAverage(check) < getDifferenceAverage(longestList) ) { - longest = check.size(); - longestList = check; - } - } - if( longestList == null ) return -1f; - if( longestList.size() == 1 && values.size() > 1 ) { - // one value only, no average needed.. but what to pick? - // just pick the smallest one... or integer, if we want that instead - if( closestToInteger ) { - float closestIntDiff = 1f; - float result = arr.getQuick(0); - for(int i=0;i songTime) { + duration = songTime; } - return getMostCommon(offsets, groupBy, false); - } - - public void AddCommonBPMs(TFloatArrayList common, TFloatArrayList times, float doubleSpeed, float timePerSample) { - float commonBPM = 60f / getMostCommon(calculateDifferences(times, doubleSpeed), timePerSample, true); - if( commonBPM > MAX_BPM ) { - common.add(commonBPM * 0.5f); - } else if( commonBPM < MIN_BPM / 2f ) { - common.add(commonBPM * 4f); - } else if( commonBPM < MIN_BPM ) { - common.add(commonBPM * 2f); - } else common.add(commonBPM); + return duration; } - - public static float tappedOffset; - public int getTappedBPM(String filename) { - // now we load the whole song so we don't have to worry about streaming a variable mp3 with timing inaccuracies - System.out.println("Loading whole song for tapping..."); - AudioSample fullSong = minim.loadSample(filename); - System.out.println("\n********************************************************************\n\nPress [ENTER] to start song, then press [ENTER] to tap to the beat.\nIt will complete after 30 entries.\nDon't worry about hitting the first beat, just start anytime.\n\n********************************************************************"); - TFloatArrayList positions = new TFloatArrayList(); - Scanner in = new Scanner(System.in); - try { - in.nextLine(); - } catch(Exception e) { } - // get the most accurate start time as possible - long nano = System.nanoTime(); - fullSong.trigger(); - nano = (System.nanoTime() + nano) / 2; - try { - for(int i=0;i<30;i++) { - in.nextLine(); - // get two playtime values & average them together for accuracy - long now = System.nanoTime(); - // calculate the time difference - // we note a consistent 0.11 second delay in input to song here - double time = (double)((now - nano) / 1000000000.0) + TAPSYNC; - positions.add((float)time); - System.out.println("#" + positions.size() + "/30: " + time + "s"); - } - } catch(Exception e) { } - fullSong.stop(); - fullSong.close(); - float avg = ((positions.getQuick(positions.size()-1) - positions.getQuick(0)) / (positions.size() - 1)); - int BPM = (int)Math.floor(60f / avg); - float timePerBeat = 60f / BPM; - tappedOffset = -getBestOffset(timePerBeat, positions, 0.1f); - return BPM; - } - - void analyzeUsingAudioRecordingStream(File filename, float seconds, String outputDir) { - int fftSize = 512; - - System.out.println("\n[--- Processing " + seconds + "s of "+ filename.getName() + " ---]"); - AudioRecordingStream stream = minim.loadFileStream(filename.getAbsolutePath(), fftSize, false); - - // tell it to "play" so we can read from it. - stream.play(); - // create the fft/beatdetect objects we'll use for analysis - BeatDetect manybd = new BeatDetect(BeatDetect.FREQ_ENERGY, fftSize, stream.getFormat().getSampleRate()); - BeatDetect fewbd = new BeatDetect(BeatDetect.FREQ_ENERGY, fftSize, stream.getFormat().getSampleRate()); - BeatDetect manybde = new BeatDetect(BeatDetect.SOUND_ENERGY, fftSize, stream.getFormat().getSampleRate()); - BeatDetect fewbde = new BeatDetect(BeatDetect.SOUND_ENERGY, fftSize, stream.getFormat().getSampleRate()); - manybd.setSensitivity(BPM_SENSITIVITY); - manybde.setSensitivity(BPM_SENSITIVITY); - fewbd.setSensitivity(60f/MAX_BPM); - fewbde.setSensitivity(60f/MAX_BPM); - - FFT fft = new FFT( fftSize, stream.getFormat().getSampleRate() ); + void analyzeUsingAudioRecordingStream(File filename, float songLengthLimitSeconds, String outputDir) { - // create the buffer we use for reading from the stream - MultiChannelBuffer buffer = new MultiChannelBuffer(fftSize, stream.getFormat().getChannels()); + // String originalNotes = OgStepGenerator.GenerateNotes(2, 2, manyTimes, fewTimes, MidFFTAmount, + // MidFFTMaxes, timePerSample, timePerBeat, startTime, songLengthLimitSeconds, false, volume); - // figure out how many samples are in the stream so we can allocate the correct number of spectra - float songTime = stream.getMillisecondLength() / 1000f; - int totalSamples = (int)( songTime * stream.getFormat().getSampleRate() ); - float timePerSample = fftSize / stream.getFormat().getSampleRate(); + TFloatArrayList startingPoint = new TFloatArrayList(); - // now we'll analyze the samples in chunks - int totalChunks = (totalSamples / fftSize) + 1; + startingPoint.insert(AlgorithmParameter.JUMP_THRESHOLD.value(), 2.0f); + startingPoint.insert(AlgorithmParameter.TAP_THRESHOLD.value(), 1.0f); + startingPoint.insert(AlgorithmParameter.SUSTAIN_THESHOLD.value(), 1.0f); + startingPoint.insert(AlgorithmParameter.SUSTAIN_FACTOR.value(), 0.2f); + startingPoint.insert(AlgorithmParameter.GRANULARITY_MODIFIER.value(), 0.98f); + startingPoint.insert(AlgorithmParameter.PRECISE_GRANULARITY_MODIFIER.value(), 0.5f); + startingPoint.insert(AlgorithmParameter.FIRST_VOLUME_THRESHOLD.value(), 0.4f); + startingPoint.insert(AlgorithmParameter.SECOND_VOLUME_THRESHOLD.value(), 0.8f); + startingPoint.insert(AlgorithmParameter.FFT_MAX_THRESHOLD.value(), 0.8f); + startingPoint.insert(AlgorithmParameter.KICK_LOW_FREQ.value(), 1f); + startingPoint.insert(AlgorithmParameter.KICK_HIGH_FREQ.value(), 6f); + startingPoint.insert(AlgorithmParameter.KICK_BAND_FREQ.value(), 2f); + startingPoint.insert(AlgorithmParameter.SNARE_LOW_FREQ.value(), 8f); + startingPoint.insert(AlgorithmParameter.SNARE_HIGH_FREQ.value(), 26f); + startingPoint.insert(AlgorithmParameter.SNARE_BAND_FREQ.value(), 6f); + startingPoint.insert(AlgorithmParameter.HAT_LOW_FREQ.value(), 20f); + startingPoint.insert(AlgorithmParameter.HAT_HIGH_FREQ.value(), 26f); + startingPoint.insert(AlgorithmParameter.HAT_BAND_FREQ.value(), 1f); + if (startingPoint.size() != (AlgorithmParameter.HAT_BAND_FREQ.value() + 1)) + { + throw new RuntimeException("Not enough parameters configured!"); + } - System.out.println("Performing Beat Detection..."); - for(int i=0;i max ) max = bandamp; + StepGenerator stepGenerator = new StepGenerator(startingPoint); + Song song = new Song(filename.getAbsolutePath()); + String newNotes = ""; + if (TRAIN) + { + int STEP_GRANULARITY = 4; + GeneticOptimizer geneticOptimizer = new GeneticOptimizer(); + TFloatArrayList optimalParameters = geneticOptimizer.optimize(STEP_GRANULARITY, startingPoint); + ArrayList> result = stepGenerator.GenerateNotes(song, SimfileDifficulty.HARD, STEP_GRANULARITY, + false, optimalParameters, -1f); + newNotes = SmFileParser.EncodeArrowLines(result, STEP_GRANULARITY); } - if( max > largestMax ) largestMax = max; - if( avg > largestAvg ) largestAvg = avg; - MidFFTAmount.add(avg); - MidFFTMaxes.add(max); - // store basic percussion times - if(manybd.isKick()) manyTimes[KICKS].add(time); - if(manybd.isHat()) manyTimes[HAT].add(time); - if(manybd.isSnare()) manyTimes[SNARE].add(time); - if(manybde.isOnset()) manyTimes[ENERGY].add(time); - if(fewbd.isKick()) fewTimes[KICKS].add(time); - if(fewbd.isHat()) fewTimes[HAT].add(time); - if(fewbd.isSnare()) fewTimes[SNARE].add(time); - if(fewbde.isOnset()) fewTimes[ENERGY].add(time); - } - System.out.println("Loudest midrange average to normalize to 1: " + largestAvg); - System.out.println("Loudest midrange maximum to normalize to 1: " + largestMax); - float scaleBy = 1f / largestAvg; - float scaleMaxBy = 1f / largestMax; - for(int i=0;i> result = stepGenerator.GenerateNotes(song, SimfileDifficulty.HARD, STEP_GRANULARITY, + false, startingPoint, -1f); + newNotes = SmFileParser.EncodeArrowLines(result, STEP_GRANULARITY); + System.out.println("Time elapsed: " + (System.currentTimeMillis() - jazzMusicStarts) / 1000f + "s"); } - // give extra weight to fewKicks - float kickStartTime = getBestOffset(timePerBeat, fewTimes[KICKS], 0.01f); - startTimes.add(kickStartTime); - startTimes.add(kickStartTime); - startTime = -getMostCommon(startTimes, 0.02f, false); - } - System.out.println("Time per beat: " + timePerBeat + ", BPM: " + BPM); - System.out.println("Start Time: " + startTime); - - // start making the SM - BufferedWriter smfile = SMGenerator.GenerateSM(BPM, startTime, filename, outputDir); - - if( HARDMODE ) System.out.println("Hard mode enabled! Extra steps for you! :-O"); - - SMGenerator.AddNotes(smfile, SMGenerator.Beginner, StepGenerator.GenerateNotes(1, HARDMODE ? 2 : 4, manyTimes, fewTimes, MidFFTAmount, MidFFTMaxes, timePerSample, timePerBeat, startTime, seconds, false)); - SMGenerator.AddNotes(smfile, SMGenerator.Easy, StepGenerator.GenerateNotes(1, HARDMODE ? 1 : 2, manyTimes, fewTimes, MidFFTAmount, MidFFTMaxes, timePerSample, timePerBeat, startTime, seconds, false)); - SMGenerator.AddNotes(smfile, SMGenerator.Medium, StepGenerator.GenerateNotes(2, HARDMODE ? 4 : 6, manyTimes, fewTimes, MidFFTAmount, MidFFTMaxes, timePerSample, timePerBeat, startTime, seconds, false)); - SMGenerator.AddNotes(smfile, SMGenerator.Hard, StepGenerator.GenerateNotes(2, HARDMODE ? 2 : 4, manyTimes, fewTimes, MidFFTAmount, MidFFTMaxes, timePerSample, timePerBeat, startTime, seconds, false)); - SMGenerator.AddNotes(smfile, SMGenerator.Challenge, StepGenerator.GenerateNotes(2, HARDMODE ? 1 : 2, manyTimes, fewTimes, MidFFTAmount, MidFFTMaxes, timePerSample, timePerBeat, startTime, seconds, true)); - SMGenerator.Complete(smfile); - - System.out.println("[--------- SUCCESS ----------]"); + + float BPM = stepGenerator.getBPM(); + float startTime = stepGenerator.getStartTime(); + + // start making the SM + BufferedWriter smfile = SMGenerator.GenerateSM(BPM, startTime, filename, outputDir); + SMGenerator.AddNotes(smfile, SMGenerator.Hard, newNotes); + SMGenerator.Complete(smfile); + + System.out.println("[--------- SUCCESS ----------]"); } } diff --git a/src/autostepper/OgStepGenerator.java b/src/autostepper/OgStepGenerator.java new file mode 100644 index 0000000..099ace2 --- /dev/null +++ b/src/autostepper/OgStepGenerator.java @@ -0,0 +1,332 @@ +package autostepper; + +import gnu.trove.list.array.TFloatArrayList; +import java.util.ArrayList; +import java.util.Random; + +import autostepper.vibejudges.SoundParameter; + +/** + * + * @author Phr00t + */ +public class OgStepGenerator { + + static private int MAX_HOLD_BEAT_COUNT = 4; + static private char EMPTY = '0', STEP = '1', HOLD = '2', STOP = '3', MINE = 'M'; + + static private int lastHoldIndex = 0; + static private int lastStepIndex = 0; + static private int lastSkipRoll = 0; + + static Random rand = new Random(); + + static private int getHoldCount() { + int ret = 0; + if( holding[0] > 0f ) ret++; + if( holding[1] > 0f ) ret++; + if( holding[2] > 0f ) ret++; + if( holding[3] > 0f ) ret++; + return ret; + } + + static private int getRandomHold() { + int hc = getHoldCount(); + if(hc == 0) return -1; + int pickHold; + if( AutoStepper.RANDOMIZED ) + { + pickHold = rand.nextInt(hc); + } + else + { + pickHold = lastHoldIndex++ % hc; + } + for(int i=0;i<4;i++) { + if( holding[i] > 0f ) { + if( pickHold == 0 ) return i; + pickHold--; + } + } + return -1; + } + + // make a note line, with lots of checks, balances & filtering + static float[] holding = new float[4]; + static float lastJumpTime; + static ArrayList AllNoteLines = new ArrayList<>(); + static float lastKickTime = 0f; + static int commaSeperator, commaSeperatorReset, mineCount, holdRun; + + private static char[] getHoldStops(int currentHoldCount, float time, int holds) { + char[] holdstops = new char[4]; + holdstops[0] = '0'; + holdstops[1] = '0'; + holdstops[2] = '0'; + holdstops[3] = '0'; + if( currentHoldCount > 0 ) { + while( holds < 0 ) { + int index = getRandomHold(); + if( index == -1 ) { + holds = 0; + currentHoldCount = 0; + } else { + holding[index] = 0f; + holdstops[index] = STOP; + holds++; currentHoldCount--; + } + } + // if we still have holds, subtract counter until 0 + for(int i=0;i<4;i++) { + if( holding[i] > 0f ) { + holding[i] -= 1f; + if( holding[i] <= 0f ) { + holding[i] = 0f; + holdstops[i] = STOP; + currentHoldCount--; + } + } + } + } + return holdstops; + } + + private static String getNoteLineIndex(int i) { + if( i < 0 || i >= AllNoteLines.size() ) return "0000"; + return String.valueOf(AllNoteLines.get(i)); + } + + private static String getLastNoteLine() { + return getNoteLineIndex(AllNoteLines.size()-1); + } + + private static void makeNoteLine(String lastLine, float time, int steps, int holds, boolean mines) { + if( steps == 0 ) { + char[] ret = getHoldStops(getHoldCount(), time, holds); + AllNoteLines.add(ret); + return; + } + if( steps > 1 && time - lastJumpTime < (mines ? 2f : 4f) ) steps = 1; // don't spam jumps + if( steps >= 2 ) { + // no hands + steps = 2; + lastJumpTime = time; + } + // can't hold or step more than 2 + int currentHoldCount = getHoldCount(); + if( holds + currentHoldCount > 2 ) holds = 2 - currentHoldCount; + if( steps + currentHoldCount > 2 ) steps = 2 - currentHoldCount; + // if we have had a run of 3 holds, don't make a new hold to prevent player from spinning + if( holdRun >= 2 && holds > 0 ) holds = 0; + // are we stopping holds? + char[] noteLine = getHoldStops(currentHoldCount, time, holds); + // if we are making a step, but just coming off a hold, move that hold end up to give proper + // time to make move to new step + if( steps > 0 && lastLine.contains("3") ) { + int currentIndex = AllNoteLines.size()-1; + char[] currentLine = AllNoteLines.get(currentIndex); + for(int i=0;i<4;i++) { + if( currentLine[i] == '3' ) { + // got a hold stop here, lets move it up + currentLine[i] = '0'; + char[] nextLineUp = AllNoteLines.get(currentIndex-1); + if( nextLineUp[i] == '2' ) { + nextLineUp[i] = '1'; + } else nextLineUp[i] = '3'; + } + } + } + // ok, make the steps + String completeLine; + char[] orig = new char[4]; + orig[0] = noteLine[0]; + orig[1] = noteLine[1]; + orig[2] = noteLine[2]; + orig[3] = noteLine[3]; + float[] willhold = new float[4]; + do { + int stepcount = steps, holdcount = holds; + noteLine[0] = orig[0]; + noteLine[1] = orig[1]; + noteLine[2] = orig[2]; + noteLine[3] = orig[3]; + willhold[0] = 0f; + willhold[1] = 0f; + willhold[2] = 0f; + willhold[3] = 0f; + while(stepcount > 0) { + int stepindex; + if( AutoStepper.RANDOMIZED ) + { + stepindex = rand.nextInt(4); + } + else + { + stepindex = lastStepIndex++ % 4; + } + if( noteLine[stepindex] != EMPTY || holding[stepindex] > 0f ) continue; + if( holdcount > 0 ) { + noteLine[stepindex] = HOLD; + willhold[stepindex] = MAX_HOLD_BEAT_COUNT; + holdcount--; stepcount--; + } else { + noteLine[stepindex] = STEP; + stepcount--; + } + } + // put in a mine? + if( mines ) { + mineCount--; + if( mineCount <= 0 ) { + mineCount = rand.nextInt(8); + if( rand.nextInt(8) == 0 && noteLine[0] == EMPTY && holding[0] <= 0f ) noteLine[0] = MINE; + if( rand.nextInt(8) == 0 && noteLine[1] == EMPTY && holding[1] <= 0f ) noteLine[1] = MINE; + if( rand.nextInt(8) == 0 && noteLine[2] == EMPTY && holding[2] <= 0f ) noteLine[2] = MINE; + if( rand.nextInt(8) == 0 && noteLine[3] == EMPTY && holding[3] <= 0f ) noteLine[3] = MINE; + } + } + completeLine = String.valueOf(noteLine); + } while( completeLine.equals(lastLine) && completeLine.equals("0000") == false ); + if( willhold[0] > holding[0] ) holding[0] = willhold[0]; + if( willhold[1] > holding[1] ) holding[1] = willhold[1]; + if( willhold[2] > holding[2] ) holding[2] = willhold[2]; + if( willhold[3] > holding[3] ) holding[3] = willhold[3]; + if( getHoldCount() == 0 ) { + holdRun = 0; + } else holdRun++; + AllNoteLines.add(noteLine); + } + + private static boolean isNearATime(float time, TFloatArrayList timelist, float threshold) { + for(int i=0;i time + threshold ) return false; + } + return false; + } + + private static float getFFT(float time, TFloatArrayList FFTAmounts, float timePerFFT) { + int index = Math.round(time / timePerFFT); + if( index < 0 || index >= FFTAmounts.size()) return 0f; + return FFTAmounts.getQuick(index); + } + + private static boolean sustainedFFT(float startTime, float len, float granularity, float timePerFFT, TFloatArrayList FFTMaxes, TFloatArrayList FFTAvg, float aboveAvg, float averageMultiplier) { + int endIndex = (int)Math.floor((startTime + len) / timePerFFT); + if( endIndex >= FFTMaxes.size() ) return false; + int wiggleRoom = Math.round(0.1f * len / timePerFFT); + int startIndex = (int)Math.floor(startTime / timePerFFT); + int pastGranu = (int)Math.floor((startTime + granularity) / timePerFFT); + boolean startThresholdReached = false; + for(int i=startIndex;i<=endIndex;i++) { + float amt = FFTMaxes.getQuick(i); + float avg = FFTAvg.getQuick(i) * averageMultiplier; + if( i <= pastGranu ) { + startThresholdReached |= amt >= avg + aboveAvg; + } else { + if( startThresholdReached == false ) return false; + if( amt < avg ) { + wiggleRoom--; + if( wiggleRoom <= 0 ) return false; + } + } + } + return true; + } + + public static String GenerateNotes(int stepGranularity, int skipChance, + TFloatArrayList[] manyTimes, + TFloatArrayList[] fewTimes, + TFloatArrayList FFTAverages, TFloatArrayList FFTMaxes, float timePerFFT, + float timePerBeat, float timeOffset, float totalTime, + boolean allowMines, TFloatArrayList volume) { + // reset variables + AllNoteLines.clear(); + lastJumpTime = -10f; + holdRun = 0; + holding[0] = 0f; + holding[1] = 0f; + holding[2] = 0f; + holding[3] = 0f; + lastKickTime = 0f; + commaSeperatorReset = 4 * stepGranularity; + float lastSkippedTime = -10f; + int totalStepsMade = 0, timeIndex = 0; + boolean skippedLast = false; + float timeGranularity = timePerBeat / stepGranularity; + for(float t = timeOffset; t <= totalTime; t += timeGranularity) { + int steps = 0, holds = 0; + String lastLine = getLastNoteLine(); + if( t > 0f ) { + float fftavg = getFFT(t, FFTAverages, timePerFFT); + float fftmax = getFFT(t, FFTMaxes, timePerFFT); + boolean sustained = sustainedFFT(t, 0.75f, timeGranularity, timePerFFT, FFTMaxes, FFTAverages, 0.25f, 0.45f); + boolean nearKick = isNearATime(t, fewTimes[SoundParameter.KICKS.value()], timePerBeat / stepGranularity); + boolean nearSnare = isNearATime(t, fewTimes[SoundParameter.SNARE.value()], timePerBeat / stepGranularity); + boolean nearEnergy = isNearATime(t, fewTimes[SoundParameter.BEAT.value()], timePerBeat / stepGranularity); + steps = sustained || nearKick || nearSnare || nearEnergy ? 1 : 0; + if( sustained ) { + holds = 1 + (nearEnergy ? 1 : 0); + } else if( fftmax < 0.5f ) { + holds = fftmax < 0.25f ? -2 : -1; + } + if( nearKick && (nearSnare || nearEnergy) && timeIndex % 2 == 0 && + steps > 0 && lastLine.contains("1") == false && lastLine.contains("2") == false && lastLine.contains("3") == false ) { + // only jump in high areas, on solid beats (not half beats) + steps = 2; + } + // wait, are we skipping new steps? + // if we just got done from a jump, don't have a half beat + // if we are holding something, don't do half-beat steps + int skipRoll; + if( AutoStepper.RANDOMIZED ) + { + skipRoll = rand.nextInt(skipChance); + } + else + { + skipRoll = lastSkipRoll++ % skipChance; + } + if( timeIndex % 2 == 1 && + (skipChance > 1 && timeIndex % 2 == 1 && skipRoll > 0 || getHoldCount() > 0) || + t - lastJumpTime < timePerBeat ) { + steps = 0; + if( holds > 0 ) holds = 0; + } + } + // if( SoundEvent.DEBUG_STEPS ) + // { + // makeNoteLine(lastLine, t, timeIndex % 2 == 0 ? 1 : 0, -2, allowMines); + // } else + { + makeNoteLine(lastLine, t, steps, holds, allowMines); + } + totalStepsMade += steps; + timeIndex++; + } + // ok, put together AllNotes + String AllNotes = ""; + commaSeperator = commaSeperatorReset; + for(int i=0;i 0 ) { + AllNotes += "3333"; + commaSeperator--; + if( commaSeperator > 0 ) AllNotes += "\n"; + } + int _stepCount = AllNotes.length() - AllNotes.replace("1", "").length(); + int _holdCount = AllNotes.length() - AllNotes.replace("2", "").length(); + int _mineCount = AllNotes.length() - AllNotes.replace("M", "").length(); + System.out.println("Steps: " + _stepCount + ", Holds: " + _holdCount + ", Mines: " + _mineCount); + return AllNotes; + } + +} \ No newline at end of file diff --git a/src/autostepper/SMGenerator.java b/src/autostepper/SMGenerator.java index 76d9732..bf65334 100644 --- a/src/autostepper/SMGenerator.java +++ b/src/autostepper/SMGenerator.java @@ -14,6 +14,8 @@ import java.io.InputStream; import java.io.OutputStream; +import autostepper.image.GoogleImageSearch; + /** * * @author Phr00t @@ -119,13 +121,14 @@ public static BufferedWriter GenerateSM(float BPM, float startTime, File songfil File imgFile = new File(dir, filename + "_img.png"); String imgFileName = ""; if( imgFile.exists() == false ) { - System.out.println("Attempting to get image for background & banner..."); - GoogleImageSearch.FindAndSaveImage(songname.replace("(", " ").replace(")", " ").replace("www.", " ").replace("_", " ").replace("-", " ").replace("&", " ").replace("[", " ").replace("]", " "), imgFile.getAbsolutePath()); + // Probably it's outdated now as I cannot make it download anything right now + // System.out.println("Attempting to get image for background & banner..."); + // GoogleImageSearch.FindAndSaveImage(songname.replace("(", " ").replace(")", " ").replace("www.", " ").replace("_", " ").replace("-", " ").replace("&", " ").replace("[", " ").replace("]", " "), imgFile.getAbsolutePath()); } if( imgFile.exists() ) { System.out.println("Got an image file!"); imgFileName = imgFile.getName(); - } else System.out.println("No image file to use :("); + } try { smfile.delete(); copyFileUsingStream(songfile, new File(dir, filename)); diff --git a/src/autostepper/StepGenerator.java b/src/autostepper/StepGenerator.java index 4fdc2f5..6e23b8a 100644 --- a/src/autostepper/StepGenerator.java +++ b/src/autostepper/StepGenerator.java @@ -1,297 +1,260 @@ package autostepper; import gnu.trove.list.array.TFloatArrayList; + import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Random; +import java.util.Scanner; +import java.util.stream.Collectors; + +import autostepper.moveassigners.SimfileDifficulty; +import autostepper.musiceventsdetector.CMusicEventsDetector; +import autostepper.genetic.AlgorithmParameter; +import autostepper.misc.Utils; +import autostepper.moveassigners.CStepAssigner; +import autostepper.moveassigners.ParametrizedAssigner; +import autostepper.vibejudges.ExcitedByEverythingJudge; +import autostepper.vibejudges.DeafToBeatJudge; +import autostepper.vibejudges.IVibeJudge; +import autostepper.vibejudges.ParametrizedJudge; +import autostepper.vibejudges.PopJudge; +import autostepper.vibejudges.SoundParameter; +import autostepper.vibejudges.VibeScore; +import autostepper.soundprocessing.CExperimentalSoundProcessor; +import autostepper.soundprocessing.Song; +import ddf.minim.AudioSample; /** * * @author Phr00t */ public class StepGenerator { - - static private int MAX_HOLD_BEAT_COUNT = 4; - static private char EMPTY = '0', STEP = '1', HOLD = '2', STOP = '3', MINE = 'M'; - - static Random rand = new Random(); - - static private int getHoldCount() { - int ret = 0; - if( holding[0] > 0f ) ret++; - if( holding[1] > 0f ) ret++; - if( holding[2] > 0f ) ret++; - if( holding[3] > 0f ) ret++; - return ret; + + CExperimentalSoundProcessor soundProcessor; + private float granularityModifier; + private float preciseGranularityModifier; + private ArrayList judges; + private ArrayList moveAssigners; + + public StepGenerator(TFloatArrayList params) + { + granularityModifier = params.get(AlgorithmParameter.GRANULARITY_MODIFIER.value()); + preciseGranularityModifier = params.get(AlgorithmParameter.PRECISE_GRANULARITY_MODIFIER.value()); + + judges = new ArrayList<>(); + + judges.add(new ParametrizedJudge(params.get(AlgorithmParameter.FIRST_VOLUME_THRESHOLD.value()), + params.get(AlgorithmParameter.SECOND_VOLUME_THRESHOLD.value()), + params.get(AlgorithmParameter.FFT_MAX_THRESHOLD.value()))); + + // Just like with vibe coding :) + // Idea is to assign vibes if current note id drop-like, standard, or it's sustained which is a candidate for hold + // For now only checking if something should happen, not exactly on which arrow + // So that later on it could be assigned to exact arrow, so '0210' or '1002' etc. + + // judges.add(new PopJudge()); + // judges.add(new ExcitedByEverythingJudge()); + // judges.add(new DeafToBeatJudge()); + + moveAssigners = new ArrayList<>(); + // moveAssigners.add(new PopStepAssigner()); + // moveAssigners.add(new LazyPopStepAssigner()); + // moveAssigners.add(new MoreTapsAssigner()); + int jumpThreshold = Math.round(params.get(AlgorithmParameter.JUMP_THRESHOLD.value())); + int tapThreshold = Math.round(params.get(AlgorithmParameter.TAP_THRESHOLD.value())); + int sustainThreshold = Math.round(params.get(AlgorithmParameter.SUSTAIN_THESHOLD.value())); + moveAssigners.add(new ParametrizedAssigner(jumpThreshold, tapThreshold, sustainThreshold)); + + soundProcessor = new CExperimentalSoundProcessor(params); } - - static private int getRandomHold() { - int hc = getHoldCount(); - if(hc == 0) return -1; - int pickHold = rand.nextInt(hc); - for(int i=0;i<4;i++) { - if( holding[i] > 0f ) { - if( pickHold == 0 ) return i; - pickHold--; - } - } - return -1; + + static String redStep(int step) { + // step: 0–9 + int[] colors = { + 231, // white + 224, // very light pink + 217, + 210, + 203, + 196, // strong red + 160, + 124, + 88, + 52 // dark red + }; + return "\u001B[38;5;" + colors[step] + "m"; } - - // make a note line, with lots of checks, balances & filtering - static float[] holding = new float[4]; - static float lastJumpTime; - static ArrayList AllNoteLines = new ArrayList<>(); - static float lastKickTime = 0f; - static int commaSeperator, commaSeperatorReset, mineCount, holdRun; - - private static char[] getHoldStops(int currentHoldCount, float time, int holds) { - char[] holdstops = new char[4]; - holdstops[0] = '0'; - holdstops[1] = '0'; - holdstops[2] = '0'; - holdstops[3] = '0'; - if( currentHoldCount > 0 ) { - while( holds < 0 ) { - int index = getRandomHold(); - if( index == -1 ) { - holds = 0; - currentHoldCount = 0; - } else { - holding[index] = 0f; - holdstops[index] = STOP; - holds++; currentHoldCount--; + + public void preview(String filename, ArrayList> detected, long stepGranularity, float timePerBeat, TFloatArrayList FFTAverages, TFloatArrayList FFTMaxes, TFloatArrayList volume, float totalTime) + { + AudioSample fullSong = AutoStepper.minimLib.loadSample(filename); + + // get the most accurate start time as possible + long millis = System.currentTimeMillis(); + long jazzMusicStarts = System.currentTimeMillis(); + // Just an assumption how long it can take to start music + millis += 1; + fullSong.trigger(); + int fftLength = FFTMaxes.size(); + + try + { + String RESET = "\u001B[0m"; + long timeGranularity = (long)(1000f * timePerBeat) / stepGranularity; + int idx = 0; + for (Map map : detected) + { + StringBuilder sb = new StringBuilder(); + sb.append((boolean)map.get(SoundParameter.KICKS) ? "K" : " "); + sb.append((boolean)map.get(SoundParameter.SNARE) ? "S" : " "); + // sb.append((boolean)map.get(SoundParameter.HAT) ? "H" : " "); + if ((boolean)map.get(SoundParameter.HALF_BEAT)) + { + sb.append("b"); } - } - // if we still have holds, subtract counter until 0 - for(int i=0;i<4;i++) { - if( holding[i] > 0f ) { - holding[i] -= 1f; - if( holding[i] <= 0f ) { - holding[i] = 0f; - holdstops[i] = STOP; - currentHoldCount--; - } - } - } - } - return holdstops; - } - - private static String getNoteLineIndex(int i) { - if( i < 0 || i >= AllNoteLines.size() ) return "0000"; - return String.valueOf(AllNoteLines.get(i)); - } - - private static String getLastNoteLine() { - return getNoteLineIndex(AllNoteLines.size()-1); - } - - private static void makeNoteLine(String lastLine, float time, int steps, int holds, boolean mines) { - if( steps == 0 ) { - char[] ret = getHoldStops(getHoldCount(), time, holds); - AllNoteLines.add(ret); - return; - } - if( steps > 1 && time - lastJumpTime < (mines ? 2f : 4f) ) steps = 1; // don't spam jumps - if( steps >= 2 ) { - // no hands - steps = 2; - lastJumpTime = time; - } - // can't hold or step more than 2 - int currentHoldCount = getHoldCount(); - if( holds + currentHoldCount > 2 ) holds = 2 - currentHoldCount; - if( steps + currentHoldCount > 2 ) steps = 2 - currentHoldCount; - // if we have had a run of 3 holds, don't make a new hold to prevent player from spinning - if( holdRun >= 2 && holds > 0 ) holds = 0; - // are we stopping holds? - char[] noteLine = getHoldStops(currentHoldCount, time, holds); - // if we are making a step, but just coming off a hold, move that hold end up to give proper - // time to make move to new step - if( steps > 0 && lastLine.contains("3") ) { - int currentIndex = AllNoteLines.size()-1; - char[] currentLine = AllNoteLines.get(currentIndex); - for(int i=0;i<4;i++) { - if( currentLine[i] == '3' ) { - // got a hold stop here, lets move it up - currentLine[i] = '0'; - char[] nextLineUp = AllNoteLines.get(currentIndex-1); - if( nextLineUp[i] == '2' ) { - nextLineUp[i] = '1'; - } else nextLineUp[i] = '3'; + else if ((boolean)map.get(SoundParameter.BEAT)) + { + sb.append("B"); } - } - } - // ok, make the steps - String completeLine; - char[] orig = new char[4]; - orig[0] = noteLine[0]; - orig[1] = noteLine[1]; - orig[2] = noteLine[2]; - orig[3] = noteLine[3]; - float[] willhold = new float[4]; - do { - int stepcount = steps, holdcount = holds; - noteLine[0] = orig[0]; - noteLine[1] = orig[1]; - noteLine[2] = orig[2]; - noteLine[3] = orig[3]; - willhold[0] = 0f; - willhold[1] = 0f; - willhold[2] = 0f; - willhold[3] = 0f; - while(stepcount > 0) { - int stepindex = rand.nextInt(4); - if( noteLine[stepindex] != EMPTY || holding[stepindex] > 0f ) continue; - if( holdcount > 0 ) { - noteLine[stepindex] = HOLD; - willhold[stepindex] = MAX_HOLD_BEAT_COUNT; - holdcount--; stepcount--; - } else { - noteLine[stepindex] = STEP; - stepcount--; + else + { + sb.append(" "); } - } - // put in a mine? - if( mines ) { - mineCount--; - if( mineCount <= 0 ) { - mineCount = rand.nextInt(8); - if( rand.nextInt(8) == 0 && noteLine[0] == EMPTY && holding[0] <= 0f ) noteLine[0] = MINE; - if( rand.nextInt(8) == 0 && noteLine[1] == EMPTY && holding[1] <= 0f ) noteLine[1] = MINE; - if( rand.nextInt(8) == 0 && noteLine[2] == EMPTY && holding[2] <= 0f ) noteLine[2] = MINE; - if( rand.nextInt(8) == 0 && noteLine[3] == EMPTY && holding[3] <= 0f ) noteLine[3] = MINE; + sb.append((boolean)map.get(SoundParameter.SUSTAINED) ? "~" : " "); + sb.append((boolean)map.get(SoundParameter.SILENCE) ? "." : " "); + sb.append((boolean)map.get(SoundParameter.NOTHING) ? "N" : " "); + sb.append(" " + idx + " / " + detected.size()); + sb.append('\t'); + String base = sb.toString(); + while (System.currentTimeMillis() - millis < timeGranularity) + { + StringBuilder sbFull = new StringBuilder(); + sbFull.append(base); + int fftIndex = (int) (((System.currentTimeMillis() - jazzMusicStarts) * fftLength) / (totalTime * 1000)); + if (fftIndex < 0) + { + fftIndex = 0; + } + if (fftIndex >= FFTMaxes.size()) + { + System.out.println("ERROR!!!!"); + fftIndex = FFTMaxes.size() - 1; + } + float fftMax = FFTMaxes.get(fftIndex); + int fftMaxLevel = (int)(fftMax*7); + sbFull.append(redStep(fftMaxLevel)); + sbFull.append(" FFTMAX: " + "=".repeat(fftMaxLevel)); + + sbFull.append('\t'); + float fftAvg = FFTAverages.get(fftIndex); + int fftAvgLevel = (int)(fftAvg*7); + sbFull.append(redStep(fftAvgLevel)); + sbFull.append(" FFTAVG: " + "=".repeat(fftAvgLevel)); + + sbFull.append('\t'); + float volumeNow = volume.get(fftIndex); + int volumeLevel = (int)(volumeNow*7); + sbFull.append(redStep(volumeLevel)); + sbFull.append(" VOL: " + "=".repeat(volumeLevel)); + + sbFull.append(RESET); + System.out.println(sbFull.toString()); + Thread.sleep(timeGranularity/10); } + idx++; + millis += timeGranularity; } - completeLine = String.valueOf(noteLine); - } while( completeLine.equals(lastLine) && completeLine.equals("0000") == false ); - if( willhold[0] > holding[0] ) holding[0] = willhold[0]; - if( willhold[1] > holding[1] ) holding[1] = willhold[1]; - if( willhold[2] > holding[2] ) holding[2] = willhold[2]; - if( willhold[3] > holding[3] ) holding[3] = willhold[3]; - if( getHoldCount() == 0 ) { - holdRun = 0; - } else holdRun++; - AllNoteLines.add(noteLine); - } - - private static boolean isNearATime(float time, TFloatArrayList timelist, float threshold) { - for(int i=0;i time + threshold ) return false; + } catch (InterruptedException e) { + e.printStackTrace(); } - return false; - } - - private static float getFFT(float time, TFloatArrayList FFTAmounts, float timePerFFT) { - int index = Math.round(time / timePerFFT); - if( index < 0 || index >= FFTAmounts.size()) return 0f; - return FFTAmounts.getQuick(index); - } - - private static boolean sustainedFFT(float startTime, float len, float granularity, float timePerFFT, TFloatArrayList FFTMaxes, TFloatArrayList FFTAvg, float aboveAvg, float averageMultiplier) { - int endIndex = (int)Math.floor((startTime + len) / timePerFFT); - if( endIndex >= FFTMaxes.size() ) return false; - int wiggleRoom = Math.round(0.1f * len / timePerFFT); - int startIndex = (int)Math.floor(startTime / timePerFFT); - int pastGranu = (int)Math.floor((startTime + granularity) / timePerFFT); - boolean startThresholdReached = false; - for(int i=startIndex;i<=endIndex;i++) { - float amt = FFTMaxes.getQuick(i); - float avg = FFTAvg.getQuick(i) * averageMultiplier; - if( i <= pastGranu ) { - startThresholdReached |= amt >= avg + aboveAvg; - } else { - if( startThresholdReached == false ) return false; - if( amt < avg ) { - wiggleRoom--; - if( wiggleRoom <= 0 ) return false; - } - } + finally { + fullSong.stop(); + fullSong.close(); } - return true; } - public static String GenerateNotes(int stepGranularity, int skipChance, - TFloatArrayList[] manyTimes, - TFloatArrayList[] fewTimes, - TFloatArrayList FFTAverages, TFloatArrayList FFTMaxes, float timePerFFT, - float timePerBeat, float timeOffset, float totalTime, - boolean allowMines) { - // reset variables - AllNoteLines.clear(); - lastJumpTime = -10f; - holdRun = 0; - holding[0] = 0f; - holding[1] = 0f; - holding[2] = 0f; - holding[3] = 0f; - lastKickTime = 0f; - commaSeperatorReset = 4 * stepGranularity; - float lastSkippedTime = -10f; - int totalStepsMade = 0, timeIndex = 0; - boolean skippedLast = false; - float timeGranularity = timePerBeat / stepGranularity; - for(float t = timeOffset; t <= totalTime; t += timeGranularity) { - int steps = 0, holds = 0; - String lastLine = getLastNoteLine(); - if( t > 0f ) { - float fftavg = getFFT(t, FFTAverages, timePerFFT); - float fftmax = getFFT(t, FFTMaxes, timePerFFT); - boolean sustained = sustainedFFT(t, 0.75f, timeGranularity, timePerFFT, FFTMaxes, FFTAverages, 0.25f, 0.45f); - boolean nearKick = isNearATime(t, fewTimes[AutoStepper.KICKS], timePerBeat / stepGranularity); - boolean nearSnare = isNearATime(t, fewTimes[AutoStepper.SNARE], timePerBeat / stepGranularity); - boolean nearEnergy = isNearATime(t, fewTimes[AutoStepper.ENERGY], timePerBeat / stepGranularity); - steps = sustained || nearKick || nearSnare || nearEnergy ? 1 : 0; - if( sustained ) { - holds = 1 + (nearEnergy ? 1 : 0); - } else if( fftmax < 0.5f ) { - holds = fftmax < 0.25f ? -2 : -1; - } - if( nearKick && (nearSnare || nearEnergy) && timeIndex % 2 == 0 && - steps > 0 && lastLine.contains("1") == false && lastLine.contains("2") == false && lastLine.contains("3") == false ) { - // only jump in high areas, on solid beats (not half beats) - steps = 2; - } - // wait, are we skipping new steps? - // if we just got done from a jump, don't have a half beat - // if we are holding something, don't do half-beat steps - if( timeIndex % 2 == 1 && - (skipChance > 1 && timeIndex % 2 == 1 && rand.nextInt(skipChance) > 0 || getHoldCount() > 0) || - t - lastJumpTime < timePerBeat ) { - steps = 0; - if( holds > 0 ) holds = 0; - } - } - if( AutoStepper.DEBUG_STEPS ) { - makeNoteLine(lastLine, t, timeIndex % 2 == 0 ? 1 : 0, -2, allowMines); - } else makeNoteLine(lastLine, t, steps, holds, allowMines); - totalStepsMade += steps; - timeIndex++; + public ArrayList> GenerateNotes(Song song, SimfileDifficulty difficulty, int stepGranularity, + boolean allowMines, TFloatArrayList params, float expectedBpm) + { + long wholeFunctionTimer = System.currentTimeMillis(); + float songTime = song.getSongTime(); + + // Frequencies matching snare, kicks etc. in really small time windows + // Here even vocal could match those frequency triggering event to be set + long timer = System.currentTimeMillis(); + TFloatArrayList[] percussionEventsInTime = soundProcessor.ProcessMusic(song, params); + if (AutoStepper.DEBUG_TIMINGS) + { + System.out.println("Initial percussion event parsing: " + (System.currentTimeMillis() - timer) / 1000f + "s"); } - // ok, put together AllNotes - String AllNotes = ""; - commaSeperator = commaSeperatorReset; - for(int i=0;i 0.6f) + { + // not as expected + return new ArrayList<>(); } } - // fill out the last empties - while( commaSeperator > 0 ) { - AllNotes += "3333"; - commaSeperator--; - if( commaSeperator > 0 ) AllNotes += "\n"; + + TFloatArrayList FFTMaxes = song.getMidFFTMaxes(); + TFloatArrayList FFTAverages = song.getMidFFTAmount(); + TFloatArrayList volume = song.getVolume(); + float timePerBeat = soundProcessor.GetTimePerBeat(); + float startTime = soundProcessor.GetStartTime(); + + // range which was doing the trick is usually between 0.2f and 0.3f + float sustainFactor = 0.35f; + CMusicEventsDetector eventsDetector = new CMusicEventsDetector(); + + // To gather info about kicks, snares etc. + // It parses whole song, so that next steps can access context + // This time using some logic, it can split vocal matching snare freq from real snare + timer = System.currentTimeMillis(); + ArrayList> NoteEvents = eventsDetector.GetEvents(stepGranularity, timePerBeat, startTime, songTime, FFTAverages, FFTMaxes, volume, song.getTimePerSample(), percussionEventsInTime, sustainFactor, granularityModifier, preciseGranularityModifier); + if (AutoStepper.DEBUG_TIMINGS) + { + System.out.println("Precise music events parsing: " + (System.currentTimeMillis() - timer) / 1000f + "s"); + } + + + if (AutoStepper.PREVIEW_DETECTION) + { + preview(song.getFilename(), NoteEvents, stepGranularity, timePerBeat, FFTAverages, FFTMaxes, volume, songTime); + } + + timer = System.currentTimeMillis(); + ArrayList> NoteVibes = judges.get(0).GetVibes(NoteEvents); + if (AutoStepper.DEBUG_TIMINGS) + { + System.out.println("Vibe assign: " + (System.currentTimeMillis() - timer) / 1000f + "s"); + } + + timer = System.currentTimeMillis(); + ArrayList> moveSet = moveAssigners.get(0).AssignMoves(NoteVibes, difficulty, songTime); + if (AutoStepper.DEBUG_TIMINGS) + { + System.out.println("Assigning moves: " + (System.currentTimeMillis() - timer) / 1000f + "s"); + System.out.println("Whole generation took: " + (System.currentTimeMillis() - wholeFunctionTimer) / 1000f + "s"); } - int _stepCount = AllNotes.length() - AllNotes.replace("1", "").length(); - int _holdCount = AllNotes.length() - AllNotes.replace("2", "").length(); - int _mineCount = AllNotes.length() - AllNotes.replace("M", "").length(); - System.out.println("Steps: " + _stepCount + ", Holds: " + _holdCount + ", Mines: " + _mineCount); - return AllNotes; + + return moveSet; + } + + public float getBPM() + { + return soundProcessor.GetBpm(); + } + + public float getStartTime() + { + return soundProcessor.GetStartTime(); } } + diff --git a/src/autostepper/genetic/AlgorithmParameter.java b/src/autostepper/genetic/AlgorithmParameter.java new file mode 100644 index 0000000..0592c69 --- /dev/null +++ b/src/autostepper/genetic/AlgorithmParameter.java @@ -0,0 +1,152 @@ +package autostepper.genetic; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import autostepper.moveassigners.ArrowPosition; + +public enum AlgorithmParameter { + JUMP_THRESHOLD(0), + TAP_THRESHOLD(1), + SUSTAIN_THESHOLD(2), + SUSTAIN_FACTOR(3), + GRANULARITY_MODIFIER(4), + PRECISE_GRANULARITY_MODIFIER(5), + FIRST_VOLUME_THRESHOLD(6), + SECOND_VOLUME_THRESHOLD(7), + FFT_MAX_THRESHOLD(8), + KICK_LOW_FREQ(9), + KICK_HIGH_FREQ(10), + KICK_BAND_FREQ(11), + SNARE_LOW_FREQ(12), + SNARE_HIGH_FREQ(13), + SNARE_BAND_FREQ(14), + HAT_LOW_FREQ(15), + HAT_HIGH_FREQ(16), + HAT_BAND_FREQ(17); + + private final int value; + + AlgorithmParameter(int value) { + this.value = value; + } + + public int value() { + return value; + } + + public static Optional maxValueForFloatParameter(AlgorithmParameter param) + { + switch (param) { + case SUSTAIN_FACTOR: + return Optional.of(1f); + case GRANULARITY_MODIFIER: + return Optional.of(1.2f); + case PRECISE_GRANULARITY_MODIFIER: + return Optional.of(1f); + case FIRST_VOLUME_THRESHOLD: + return Optional.of(0.9f); + case SECOND_VOLUME_THRESHOLD: + return Optional.of(1f); + case FFT_MAX_THRESHOLD: + return Optional.of(1f); + default: + return Optional.empty(); + } + } + + + public static Optional minValueForFloatParameter(AlgorithmParameter param) + { + switch (param) { + case SUSTAIN_FACTOR: + return Optional.of(0.1f); + case GRANULARITY_MODIFIER: + return Optional.of(0.8f); + case PRECISE_GRANULARITY_MODIFIER: + return Optional.of(0.5f); + case FIRST_VOLUME_THRESHOLD: + return Optional.of(0.2f); + case SECOND_VOLUME_THRESHOLD: + return Optional.of(0.3f); + case FFT_MAX_THRESHOLD: + return Optional.of(0.3f); + default: + return Optional.empty(); + } + } + + public static Optional maxValueForIntParameter(AlgorithmParameter param) + { + switch (param) { + case JUMP_THRESHOLD: + return Optional.of(10); + case TAP_THRESHOLD: + return Optional.of(9); + case SUSTAIN_THESHOLD: + return Optional.of(10); + case KICK_LOW_FREQ: + return Optional.of(8); + case KICK_HIGH_FREQ: + return Optional.of(10); + case KICK_BAND_FREQ: + return Optional.of(10); + case SNARE_LOW_FREQ: + return Optional.of(14); + case SNARE_HIGH_FREQ: + return Optional.of(40); + case SNARE_BAND_FREQ: + return Optional.of(10); + case HAT_LOW_FREQ: + return Optional.of(23); + case HAT_HIGH_FREQ: + return Optional.of(30); + case HAT_BAND_FREQ: + return Optional.of(2); + default: + return Optional.empty(); + } + } + + public static Optional minValueForIntParameter(AlgorithmParameter param) + { + switch (param) { + case JUMP_THRESHOLD: + return Optional.of(2); + case TAP_THRESHOLD: + return Optional.of(1); + case SUSTAIN_THESHOLD: + return Optional.of(1); + case KICK_LOW_FREQ: + return Optional.of(1); + case KICK_HIGH_FREQ: + return Optional.of(3); + case KICK_BAND_FREQ: + return Optional.of(1); + case SNARE_LOW_FREQ: + return Optional.of(4); + case SNARE_HIGH_FREQ: + return Optional.of(15); + case SNARE_BAND_FREQ: + return Optional.of(1); + case HAT_LOW_FREQ: + return Optional.of(17); + case HAT_HIGH_FREQ: + return Optional.of(20); + case HAT_BAND_FREQ: + return Optional.of(1); + default: + return Optional.empty(); + } + } + + private static final Map BY_VALUE = + Arrays.stream(values()) + .collect(Collectors.toMap(AlgorithmParameter::value, e -> e)); + + public static AlgorithmParameter fromValue(int value) { + return BY_VALUE.get(value); // may return null + } +} diff --git a/src/autostepper/genetic/GeneticOptimizer.java b/src/autostepper/genetic/GeneticOptimizer.java new file mode 100644 index 0000000..67d5002 --- /dev/null +++ b/src/autostepper/genetic/GeneticOptimizer.java @@ -0,0 +1,247 @@ +package autostepper.genetic; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Optional; +import java.util.Random; + +import autostepper.AutoStepper; +import autostepper.StepGenerator; +import autostepper.misc.Averages; +import autostepper.misc.Utils; +import autostepper.moveassigners.SimfileDifficulty; +import autostepper.smfile.SmFileParser; +import autostepper.soundprocessing.Song; +import autostepper.vibejudges.SoundParameter; +import gnu.trove.list.array.TFloatArrayList; + +public class GeneticOptimizer { + + // --- Configuration --- + static final int POPULATION_SIZE = 8; + static final int NUM_GENERATIONS = 20; + static final float MUTATION_RATE = 0.15f; + + static Random random = new Random(); + + int numberOfParams; + int stepGranularity; + private ArrayList song1Reference; + private ArrayList song2Reference; + private Song song1; + private Song song2; + + public GeneticOptimizer() { + song1Reference = SmFileParser.parseFile("Target/AbracadabraOut.sm"); + song2Reference = SmFileParser.parseFile("Target/edamameOut.sm"); + + song1 = new Song("samples/Abracadabra.mp3"); + song2 = new Song("samples/edamame.mp3"); + } + + public TFloatArrayList optimize(int stepGranularity, TFloatArrayList startingPoint) + { + float[] chromosome = startingPoint.toArray(); + numberOfParams = startingPoint.size(); + this.stepGranularity = stepGranularity; + + // Initialize population + float[][] population = new float[POPULATION_SIZE][numberOfParams]; + for (int i = 0; i < POPULATION_SIZE; i++) { + population[i] = Arrays.copyOf(chromosome, chromosome.length); + } + + ArrayList bestIndividuals = new ArrayList<>(); + int bestScore = Integer.MIN_VALUE; + + // Run generations + for (int gen = 0; gen < NUM_GENERATIONS; gen++) { + int[] scores = new int[POPULATION_SIZE]; + for (int i = 0; i < POPULATION_SIZE; i++) { + scores[i] = 0; + } + // Selection & Crossover + float[][] newPopulation = new float[POPULATION_SIZE][numberOfParams]; + for (int i = 0; i < POPULATION_SIZE; i++) { + float[] parent1 = select(population, scores); + float[] parent2 = select(population, scores); + newPopulation[i] = crossover(parent1, parent2); + mutate(newPopulation[i], false); + } + population = newPopulation; + + // Evaluate fitness + for (int i = 0; i < POPULATION_SIZE; i++) { + scores[i] = fitness(population[i]); + System.out.println("Jump\tTap\tsust\tsusf\tgran\tpgran\tvol1\tvol2\tfftmax\tkicklow\tkickhig\tkickb\tsnarlow\tsnarhig\tsnarb\thatlow\thathig\thatb"); + for (int j = 0; j < population[i].length; j++) { + System.out.print(String.format("%.3f", population[i][j]) + "\t"); + } + System.out.println(); + } + + // Find the best in this generation + for (int i = 0; i < POPULATION_SIZE; i++) { + if (scores[i] > bestScore) { + bestScore = scores[i]; + bestIndividuals.add(0, Arrays.copyOf(population[i], numberOfParams)); + if (bestIndividuals.size() > 3) + { + bestIndividuals.remove(3); + } + } + } + + System.out.println("Generation " + gen + " Best score: " + bestScore + " , best score possible: " + (song1Reference.size() + song2Reference.size())); + } + + System.out.println("Best individuals: "); + for (float[] fs : bestIndividuals) { + System.out.println("Best individuals: " + Arrays.toString(fs)); + } + System.out.println("Best score: " + bestScore); + return new TFloatArrayList(bestIndividuals.get(0)); + } + + // --- GA Components --- + int fitness(float[] chromosome) { + ArrayList abracadabraResult = getSongFingerprint(song1, chromosome, 126f); + ArrayList cureResult = getSongFingerprint(song2, chromosome, 106f); + + int numberOfDifferencesSong1 = calculateDifferences(song1Reference, abracadabraResult); + int numberOfDifferencesSong2 = calculateDifferences(song2Reference, cureResult); + + return abracadabraResult.size() + cureResult.size() - numberOfDifferencesSong1 - numberOfDifferencesSong2; + } + + private int calculateDifferences(ArrayList abracadabraReference, ArrayList abracadabraResult) { + int indexRef = 0; + int indexResult = 0; + int totalDiffs = 0; + boolean anyFound = false; + while (!anyFound) + { + if (abracadabraReference.get(indexRef) != 0) + anyFound = true; + } + anyFound = false; + while (!anyFound && indexResult < abracadabraResult.size()) + { + if (abracadabraResult.get(indexResult) != 0) + anyFound = true; + } + + if (anyFound == false) + { + return 999999; // There was not even single tap + } + + while (indexRef < abracadabraReference.size() && indexResult < abracadabraResult.size()) + { + totalDiffs += Math.abs(abracadabraReference.get(indexRef) - abracadabraResult.get(indexResult)); + indexRef++; + indexResult++; + } + return totalDiffs; + } + + private ArrayList getSongFingerprint(Song song, float[] chromosome, float expectedBpm) + { + StepGenerator stepGenerator = new StepGenerator(new TFloatArrayList(chromosome)); + ArrayList> abracadabraArrows = stepGenerator.GenerateNotes(song, SimfileDifficulty.HARD, stepGranularity, false, new TFloatArrayList(chromosome), expectedBpm); + return SmFileParser.parseLines(abracadabraArrows); + } + + float[] select(float[][] population, int[] scores) { + // Roulette wheel selection + float sum = 0; + for (int s : scores) sum += s; + float r = random.nextFloat() * sum; + int accum = 0; + for (int i = 0; i < population.length; i++) { + accum += scores[i]; + if (accum >= r) return population[i]; + } + return population[population.length - 1]; + } + + float[] crossover(float[] parent1, float[] parent2) { + float[] child = new float[numberOfParams]; + for (int i = 0; i < numberOfParams; i++) { + child[i] = random.nextBoolean() ? parent1[i] : parent2[i]; + } + return child; + } + + void + + mutate(float[] chromosome, boolean force) { + for (int i = 0; i < numberOfParams; i++) { + boolean mutatedProperly = false; + if ((random.nextFloat() < MUTATION_RATE || force) && !mutatedProperly) + { + float value = mutateSingleGene(i); + // Guards for frequency params to avoid lower freq to be higher than high freq + // Could be done by randomizing pairs, TODO + if (AlgorithmParameter.fromValue(i) == AlgorithmParameter.KICK_LOW_FREQ) + { + if (chromosome[AlgorithmParameter.KICK_HIGH_FREQ.value()] < value) + { + // higher has to be higher than low... + } + } + else if (AlgorithmParameter.fromValue(i) == AlgorithmParameter.KICK_HIGH_FREQ) + { + if (chromosome[AlgorithmParameter.KICK_LOW_FREQ.value()] > value) + { + // higher has to be higher than low... + } + } + else if (AlgorithmParameter.fromValue(i) == AlgorithmParameter.SNARE_LOW_FREQ) + { + if (chromosome[AlgorithmParameter.SNARE_HIGH_FREQ.value()] < value) + { + // higher has to be higher than low... + } + } + else if (AlgorithmParameter.fromValue(i) == AlgorithmParameter.SNARE_HIGH_FREQ) + { + if (chromosome[AlgorithmParameter.SNARE_LOW_FREQ.value()] > value) + { + // higher has to be higher than low... + } + } + else + { + mutatedProperly = true; + chromosome[i] = value; + } + } + } + } + + static public float mutateSingleGene(int i) { + Optional maxValInt = AlgorithmParameter.maxValueForIntParameter(AlgorithmParameter.fromValue(i)); + Optional maxValFloat = AlgorithmParameter.maxValueForFloatParameter(AlgorithmParameter.fromValue(i)); + float newValue; + if (maxValInt.isPresent()) + { + Optional minValInt = AlgorithmParameter.minValueForIntParameter(AlgorithmParameter.fromValue(i)); + newValue = (float)(minValInt.get() + random.nextInt(maxValInt.get() + 1 - minValInt.get())); + } + else + { + Optional minValFloat = AlgorithmParameter.minValueForFloatParameter(AlgorithmParameter.fromValue(i)); + newValue = minValFloat.get() + random.nextFloat() * (maxValFloat.get() - minValFloat.get()); + } + return newValue; + } + + // --- Replace with your actual scoring function --- + float yourScoringFunction(float[] params) { + // Example: max score if sum of params is close to 5 + float sum = 0; + for (float p : params) sum += p; + return 1.0f - Math.min(Math.abs(5 - sum) / 5.0f, 1.0f); + } +} diff --git a/src/autostepper/GoogleImageSearch.java b/src/autostepper/image/GoogleImageSearch.java similarity index 96% rename from src/autostepper/GoogleImageSearch.java rename to src/autostepper/image/GoogleImageSearch.java index 6697c2f..8492bca 100644 --- a/src/autostepper/GoogleImageSearch.java +++ b/src/autostepper/image/GoogleImageSearch.java @@ -1,50 +1,50 @@ -package autostepper; - -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URL; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -public class GoogleImageSearch { - - public static void FindAndSaveImage(String question, String destination) { - String ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0"; - String finRes = ""; - - try { - String googleUrl = "https://www.google.com/search?as_st=y&tbm=isch&as_q=" + question.replace(",", "+").replace(" ", "+") + "&as_epq=&as_oq=&as_eq=&cr=&as_sitesearch=&safe=images&tbs=isz:lt,islt:vga,iar:w"; - Document doc1 = Jsoup.connect(googleUrl).userAgent(ua).timeout(8 * 1000).get(); - Elements elems = doc1.select("[data-src]"); - if( elems.isEmpty() ) { - System.out.println("Couldn't find any images for: " + question); - return; - } - Element media = elems.first(); - String finUrl = media.attr("abs:data-src"); - saveImage(finUrl.replace(""", ""), destination); - } catch (Exception e) { - System.out.println(e); - } - } - - public static void saveImage(String imageUrl, String destinationFile) throws IOException { - URL url = new URL(imageUrl); - InputStream is = url.openStream(); - OutputStream os = new FileOutputStream(destinationFile); - - byte[] b = new byte[2048]; - int length; - - while ((length = is.read(b)) != -1) { - os.write(b, 0, length); - } - - is.close(); - os.close(); - } +package autostepper.image; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +public class GoogleImageSearch { + + public static void FindAndSaveImage(String question, String destination) { + String ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0"; + String finRes = ""; + + try { + String googleUrl = "https://www.google.com/search?as_st=y&tbm=isch&as_q=" + question.replace(",", "+").replace(" ", "+") + "&as_epq=&as_oq=&as_eq=&cr=&as_sitesearch=&safe=images&tbs=isz:lt,islt:vga,iar:w"; + Document doc1 = Jsoup.connect(googleUrl).userAgent(ua).timeout(8 * 1000).get(); + Elements elems = doc1.select("[data-src]"); + if( elems.isEmpty() ) { + System.out.println("Couldn't find any images for: " + question); + return; + } + Element media = elems.first(); + String finUrl = media.attr("abs:data-src"); + saveImage(finUrl.replace(""", ""), destination); + } catch (Exception e) { + System.out.println(e); + } + } + + public static void saveImage(String imageUrl, String destinationFile) throws IOException { + URL url = new URL(imageUrl); + InputStream is = url.openStream(); + OutputStream os = new FileOutputStream(destinationFile); + + byte[] b = new byte[2048]; + int length; + + while ((length = is.read(b)) != -1) { + os.write(b, 0, length); + } + + is.close(); + os.close(); + } } \ No newline at end of file diff --git a/src/autostepper/misc/Averages.java b/src/autostepper/misc/Averages.java new file mode 100644 index 0000000..0722e60 --- /dev/null +++ b/src/autostepper/misc/Averages.java @@ -0,0 +1,104 @@ +package autostepper.misc; + +import java.util.ArrayList; + +import gnu.trove.list.array.TFloatArrayList; + +public class Averages { + public static float getMostCommonPhr00t(TFloatArrayList inputArray, float threshold, boolean closestToInteger) + { + // Goes through input array + // then splits it into multiple lists + // It attempts to add to each list if diff with ANY element from intermediate list + // is lower than threshold + ArrayList intermediateValues = new ArrayList<>(); + for (int i = 0; i < inputArray.size(); i++) { + float inputElement = inputArray.get(i); + // check for this value in our current lists + boolean notFound = true; + for (int j = 0; j < intermediateValues.size(); j++) + { + TFloatArrayList tal = intermediateValues.get(j); + for (int k = 0; k < tal.size(); k++) { + float listValue = tal.get(k); + if (Math.abs(listValue - inputElement) < threshold) { + notFound = false; + tal.add(inputElement); + break; + } + } + if (notFound == false) + break; + } + // if it wasn't found, start a new list + if (notFound) { + TFloatArrayList newList = new TFloatArrayList(); + newList.add(inputElement); + intermediateValues.add(newList); + } + } + // get the longest list + int longest = 0; + TFloatArrayList longestList = null; + for (int i = 0; i < intermediateValues.size(); i++) { + TFloatArrayList check = intermediateValues.get(i); + if (check.size() > longest || + check.size() == longest && Utils.getDifferenceAverage(check) < Utils.getDifferenceAverage(longestList)) { + longest = check.size(); + longestList = check; + } + } + if (longestList == null) + return -1f; + if (longestList.size() == 1 && intermediateValues.size() > 1) { + // one value only, no average needed.. but what to pick? + // just pick the smallest one... or integer, if we want that instead + if (closestToInteger) { + float closestIntDiff = 1f; + float result = inputArray.getQuick(0); + for (int i = 0; i < inputArray.size(); i++) { + float diff = Math.abs(Math.round(inputArray.getQuick(i)) - inputArray.getQuick(i)); + if (diff < closestIntDiff) { + closestIntDiff = diff; + result = inputArray.getQuick(i); + } + } + return result; + } else { + float smallest = 99999f; + for (int i = 0; i < inputArray.size(); i++) { + if (inputArray.getQuick(i) < smallest) + smallest = inputArray.getQuick(i); + } + return smallest; + } + } + // calculate average + float avg = 0f; + for (int i = 0; i < longestList.size(); i++) { + avg += longestList.get(i); + } + return avg / longestList.size(); + } + + public static float getMostCommonFightlapa(TFloatArrayList inputArray, float threshold, boolean closestToInteger) + { + if (inputArray.size() == 0) + { + return 0.0f; + } + TFloatArrayList inputCopy = new TFloatArrayList(inputArray); + int inputSize = inputCopy.size(); + inputCopy.sort(); + + int itemsToTrim = Math.round(inputSize * 0.1f); // 10% + for (int i = 0; i < itemsToTrim; i++) { + inputCopy.removeAt(inputCopy.size() - 1); + } + + for (int i = 0; i < itemsToTrim; i++) { + inputCopy.removeAt(0); + } + return inputCopy.get(inputCopy.size() / 2); + } +} diff --git a/src/autostepper/misc/Utils.java b/src/autostepper/misc/Utils.java new file mode 100644 index 0000000..6312eef --- /dev/null +++ b/src/autostepper/misc/Utils.java @@ -0,0 +1,59 @@ +package autostepper.misc; + +import java.io.File; +import java.util.ArrayList; + +import autostepper.AutoStepper; +import ddf.minim.spi.AudioRecordingStream; +import gnu.trove.list.array.TFloatArrayList; + +// class - a legend, everything and nothing in every project +public class Utils +{ + public static TFloatArrayList calculateDifferences(TFloatArrayList arr, float timeThreshold) { + TFloatArrayList diff = new TFloatArrayList(); + int currentlyAt = 0; + while (currentlyAt < arr.size() - 1) { + float mytime = arr.getQuick(currentlyAt); + int oldcurrentlyat = currentlyAt; + for (int i = currentlyAt + 1; i < arr.size(); i++) { + float diffcheck = arr.getQuick(i) - mytime; + if (diffcheck >= timeThreshold) { + diff.add(diffcheck); + currentlyAt = i; + break; + } + } + if (oldcurrentlyat == currentlyAt) + break; + } + return diff; + } + + public static float getDifferenceAverage(TFloatArrayList arr) { + float avg = 0f; + for (int i = 0; i < arr.size() - 1; i++) { + avg += Math.abs(arr.getQuick(i + 1) - arr.getQuick(i)); + } + if (arr.size() <= 1) + return 0f; + return avg / arr.size() - 1; + } + + public static float getSongTime(String inputFile) { + // anything to get song length + int fftSize = 512; + + AudioRecordingStream stream = AutoStepper.minimLib.loadFileStream(inputFile, fftSize, false); + float songTime = stream.getMillisecondLength() / 1000f; + return songTime; + } + + public static float getBestOffset(float timePerBeat, TFloatArrayList times, float groupBy) { + TFloatArrayList offsets = new TFloatArrayList(); + for (int i = 0; i < times.size(); i++) { + offsets.add(times.getQuick(i) % timePerBeat); + } + return Averages.getMostCommonPhr00t(offsets, groupBy, false); + } +} diff --git a/src/autostepper/moveassigners/ArrowPosition.java b/src/autostepper/moveassigners/ArrowPosition.java new file mode 100644 index 0000000..a34e2a1 --- /dev/null +++ b/src/autostepper/moveassigners/ArrowPosition.java @@ -0,0 +1,31 @@ +package autostepper.moveassigners; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public enum ArrowPosition { + LEFT(0), + DOWN(1), + UP(2), + RIGHT(3), + NOWHERE(4); + + private final int value; + + ArrowPosition(int value) { + this.value = value; + } + + public int value() { + return value; + } + + private static final Map BY_VALUE = + Arrays.stream(values()) + .collect(Collectors.toMap(ArrowPosition::value, e -> e)); + + public static ArrowPosition fromValue(int value) { + return BY_VALUE.get(value); // may return null + } +} \ No newline at end of file diff --git a/src/autostepper/moveassigners/CStepAssigner.java b/src/autostepper/moveassigners/CStepAssigner.java new file mode 100644 index 0000000..11a2590 --- /dev/null +++ b/src/autostepper/moveassigners/CStepAssigner.java @@ -0,0 +1,42 @@ +package autostepper.moveassigners; + +import java.util.ArrayList; +import java.util.Map; +import java.util.Random; + +import autostepper.AutoStepper; +import autostepper.moveassigners.steporganizer.Foot; +import autostepper.vibejudges.VibeScore; + +public abstract class CStepAssigner { + static int ACTION_HOLD = 0; + static int TAP = 1; + static int JUMP = 2; + static int DOUBLE_HOLD = 0; + + static public char EMPTY = '0', STEP = '1', HOLD = '2', STOP = '3', MINE = 'M', KEEP_HOLDING = 'H'; + + String name; + Random rand; + + public CStepAssigner(String name) + { + if (AutoStepper.RANDOMIZED ) + { + rand = new Random(); + } + else + { + rand = new Random(123L); + } + this.name = name; + } + + public String AssignerName() + { + return name; + } + + + public abstract ArrayList> AssignMoves(ArrayList> NoteVibes, SimfileDifficulty difficulty, float totalTime); +} diff --git a/src/autostepper/moveassigners/ParametrizedAssigner.java b/src/autostepper/moveassigners/ParametrizedAssigner.java new file mode 100644 index 0000000..6248bd5 --- /dev/null +++ b/src/autostepper/moveassigners/ParametrizedAssigner.java @@ -0,0 +1,217 @@ +package autostepper.moveassigners; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import autostepper.moveassigners.steporganizer.Foot; +import autostepper.moveassigners.steporganizer.StepOrganizer; +import autostepper.vibejudges.VibeScore; + +public class ParametrizedAssigner extends CStepAssigner { + + private int jumpThreshold; + private int tapThreshold; + private int sustainThreshold; + + public ParametrizedAssigner(int jumpThreshold, int tapThreshold, int sustainThreshold) { + super("Parametrized assigned jumpsThr: " + jumpThreshold + " tapThr: " + tapThreshold + " sustainThr: " + sustainThreshold); + assert(tapThreshold < jumpThreshold); + this.jumpThreshold = jumpThreshold; + this.tapThreshold = tapThreshold; + this.sustainThreshold = sustainThreshold; + } + + @Override + public ArrayList> AssignMoves(ArrayList> NoteVibes, + SimfileDifficulty difficulty, float totalTime) + { + StepOrganizer stepOrganizer = new StepOrganizer(); + ArrayList> NoteMoves = new ArrayList<>(); + // Here it will work in context windows + // To start with something, it will search for patterns of 3 and 4 combos + // Meaning steps without empty line inbetween + int steps = 0; + int numberOfRows = NoteVibes.size(); + for (int i = 0; i < numberOfRows; i++) + { + Optional> previousLine; + if (i > 0) + { + previousLine = Optional.of(NoteMoves.get(i - 1)); + } + else + { + previousLine = Optional.empty(); + } + List actions = new ArrayList<>(); + + Map previousPreviousVibe = getVibe(NoteVibes, i, -2); + Map previousVibe = getVibe(NoteVibes, i, -1); + Map currentVibe = NoteVibes.get(i); + Map nextVibe = getVibe(NoteVibes, i, 1); + Map nextNextVibe = getVibe(NoteVibes, i, 2); + + if (currentVibe.get(VibeScore.POWER) >= jumpThreshold) + { + if ((nextVibe.get(VibeScore.POWER) < jumpThreshold) && (nextNextVibe.get(VibeScore.POWER) < jumpThreshold)) + { + actions.add(JUMP); + } + else + { + actions.add(TAP); + } + } + else if (currentVibe.get(VibeScore.POWER) >= tapThreshold) + { + actions.add(TAP); + } + if (currentVibe.get(VibeScore.SUSTAIN) >= sustainThreshold) + { + if (actions.contains(JUMP)) + { + actions.add(DOUBLE_HOLD); + actions.remove((Object)JUMP); + actions.remove((Object)TAP); + } + else + { + actions.add(ACTION_HOLD); + actions.remove((Object)TAP); + } + } + + ArrayList arrowLine = new ArrayList<>(List.of('0', '0', '0', '0')); + + boolean anyHolds = false; + // First maintain holds if there are any + anyHolds = maintainHolds(stepOrganizer, NoteMoves, i, actions, arrowLine, anyHolds); + + if (!anyHolds) + { + // Start holding + if (actions.contains(ACTION_HOLD)) + { + ArrayList stepPosition = stepOrganizer.getAvailableArrow(1, arrowLine, previousLine, true); + for (ArrowPosition arrowPosition : stepPosition) { + arrowLine.set(arrowPosition.value(), HOLD); + } + } + else if (actions.contains(DOUBLE_HOLD)) + { + // For double hold, we can consider nothing locked as you have to use hands anyways + ArrayList stepPosition = stepOrganizer.getAvailableArrow(2, arrowLine, previousLine, false); + for (ArrowPosition arrowPosition : stepPosition) { + arrowLine.set(arrowPosition.value(), HOLD); + } + } + } + + int numberOfHolds = getNumberOfHolds(arrowLine); + if (numberOfHolds == 2) + { + actions.remove((Object)JUMP); + actions.remove((Object)TAP); + } + else if (numberOfHolds == 1) + { + actions.remove((Object)JUMP); + } + + int arrows = 0; + if (actions.contains(JUMP)) + { + arrows = 2; + } + else if (actions.contains(TAP)) + { + arrows = 1; + } + + ArrayList stepPositions; + if (i > 0) + { + stepPositions = stepOrganizer.getAvailableArrow(arrows, arrowLine, previousLine, false); + } + else + { + stepPositions = stepOrganizer.getAvailableArrow(arrows, arrowLine, previousLine, false); + } + + for (ArrowPosition arrowPosition : stepPositions) { + arrowLine.set(arrowPosition.value(), STEP); + steps++; + } + + NoteMoves.add(arrowLine); + } + System.out.println("Steps: " + steps); + return NoteMoves; + } + + private int getNumberOfHolds(ArrayList arrowLine) { + int count = 0; + for (char c : arrowLine) { + if (c == HOLD || c == STOP || c == KEEP_HOLDING) { + count++; + } + } + return count; + } + + private boolean maintainHolds(StepOrganizer stepOrganizer, ArrayList> NoteMoves, int i, List actions, + ArrayList arrowLine, boolean anyHolds) { + if (i - 1 >= 0) + { + for (int j = 0; j < 4; j++) + { + if (NoteMoves.get(i - 1).get(j) == HOLD) + { + anyHolds = true; + if (actions.contains(ACTION_HOLD) || actions.contains(DOUBLE_HOLD)) + { + arrowLine.set(j, KEEP_HOLDING); + } + else + { + arrowLine.set(j, STOP); + stepOrganizer.unlock(ArrowPosition.fromValue(j)); + } + } + if (NoteMoves.get(i - 1).get(j) == KEEP_HOLDING) + { + anyHolds = true; + if (actions.contains(ACTION_HOLD)) + { + arrowLine.set(j, KEEP_HOLDING); + } + else + { + arrowLine.set(j, STOP); + stepOrganizer.unlock(ArrowPosition.fromValue(j)); + } + } + } + } + return anyHolds; + } + + private Map getVibe(ArrayList> NoteVibes, int currentVibeIdx, int offset) { + Map vibe; + if (currentVibeIdx + offset >= 0 && currentVibeIdx + offset < NoteVibes.size()) + { + vibe = NoteVibes.get(currentVibeIdx + offset); + } + else + { + vibe = new HashMap<>(); + vibe.put(VibeScore.POWER, 0); + vibe.put(VibeScore.SUSTAIN, 0); + } + return vibe; + } + +} diff --git a/src/autostepper/moveassigners/SimfileDifficulty.java b/src/autostepper/moveassigners/SimfileDifficulty.java new file mode 100644 index 0000000..559bf7c --- /dev/null +++ b/src/autostepper/moveassigners/SimfileDifficulty.java @@ -0,0 +1,31 @@ +package autostepper.moveassigners; + +public enum SimfileDifficulty { + BEGINNER(1), + EASY(2), + MEDIUM(3), + HARD(4), + CHALLENGE(5); + + private final int value; + + SimfileDifficulty(int value) { + this.value = value; + } + + public int value() { + return value; + } + + public int getExpectedStepsPerMinute() + { + switch (this) { + case BEGINNER: return 55; + case EASY: return 80; + case MEDIUM: return 105; + case HARD: return 170; + case CHALLENGE: return 190; + default: throw new RuntimeException("Wrong difficulty"); + } + } +} \ No newline at end of file diff --git a/src/autostepper/moveassigners/steporganizer/Foot.java b/src/autostepper/moveassigners/steporganizer/Foot.java new file mode 100644 index 0000000..a3c78d0 --- /dev/null +++ b/src/autostepper/moveassigners/steporganizer/Foot.java @@ -0,0 +1,15 @@ +package autostepper.moveassigners.steporganizer; + +public enum Foot { + LEFT(0), + RIGHT(1); + private final int value; + + Foot(int value) { + this.value = value; + } + + public int value() { + return value; + } +} \ No newline at end of file diff --git a/src/autostepper/moveassigners/steporganizer/StepOrganizer.java b/src/autostepper/moveassigners/steporganizer/StepOrganizer.java new file mode 100644 index 0000000..d3e93d9 --- /dev/null +++ b/src/autostepper/moveassigners/steporganizer/StepOrganizer.java @@ -0,0 +1,93 @@ +package autostepper.moveassigners.steporganizer; + +import java.util.ArrayList; +import java.util.Optional; +import java.util.Random; + +import autostepper.moveassigners.ArrowPosition; +import autostepper.moveassigners.CStepAssigner; + +public class StepOrganizer { + // To encourage jumps to be <> instead of ^> etc. + private static int PREDEFINED_CHOICE_THRESHOLD = 5; + private static Random rand = new Random(125L); + + Foot lastUsedFoot = Foot.LEFT; + ArrowPosition leftFoot = ArrowPosition.LEFT; + ArrowPosition rightFoot = ArrowPosition.RIGHT; + ArrowPosition footLocked = ArrowPosition.NOWHERE; + private int linesFromPredefinedChoice = 999; + + public ArrayList getAvailableArrow(int numberOfArrows, ArrayList arrowLine, Optional> previousLine, boolean footLock) + { + ArrayList result = new ArrayList<>(); + int arrowsLeft = numberOfArrows; + + while (arrowsLeft > 0) + { + // It's preferred to see symetrical duo, so that's what's happening there + if (arrowsLeft == 2 + && linesFromPredefinedChoice > PREDEFINED_CHOICE_THRESHOLD + && arrowLine.get(ArrowPosition.LEFT.value()) == CStepAssigner.EMPTY + && arrowLine.get(ArrowPosition.RIGHT.value()) == CStepAssigner.EMPTY ) + { + result.add(ArrowPosition.LEFT); + result.add(ArrowPosition.RIGHT); + arrowsLeft--; + arrowsLeft--; + } + else + { + ArrowPosition attempt = ArrowPosition.fromValue(rand.nextInt(4)); + if ((lastUsedFoot == Foot.LEFT && leftFoot == ArrowPosition.DOWN && attempt == ArrowPosition.LEFT) + || (lastUsedFoot == Foot.RIGHT && rightFoot == ArrowPosition.DOWN && attempt == ArrowPosition.RIGHT) + || (lastUsedFoot == Foot.LEFT && leftFoot == ArrowPosition.UP && attempt == ArrowPosition.LEFT) + || (lastUsedFoot == Foot.RIGHT && rightFoot == ArrowPosition.UP && attempt == ArrowPosition.RIGHT)) + { + continue; // Avoid crossovers, for now + } + + if( arrowLine.get(attempt.value()) != CStepAssigner.EMPTY) + { + continue; // Already busy + } + else if (previousLine.isPresent() && previousLine.get().get(attempt.value()) != CStepAssigner.EMPTY) + { + // Don't do same arrow twice + continue; + } + else + { + result.add(attempt); + arrowsLeft--; + SwitchFoot(); + } + } + } + + return result; + } + + void SwitchFoot() + { + if (footLocked != ArrowPosition.NOWHERE) + { + if (lastUsedFoot == Foot.LEFT) + { + lastUsedFoot = Foot.RIGHT; + } + else + { + lastUsedFoot = Foot.LEFT; + } + } + } + + public void unlock(ArrowPosition location) + { + if (footLocked == location) + { + footLocked = ArrowPosition.NOWHERE; + } + } +} diff --git a/src/autostepper/musiceventsdetector/CMusicEventsDetector.java b/src/autostepper/musiceventsdetector/CMusicEventsDetector.java new file mode 100644 index 0000000..2f3e143 --- /dev/null +++ b/src/autostepper/musiceventsdetector/CMusicEventsDetector.java @@ -0,0 +1,172 @@ +package autostepper.musiceventsdetector; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import autostepper.vibejudges.SoundParameter; +import gnu.trove.list.array.TFloatArrayList; + +public class CMusicEventsDetector { + int getNearestTimeSeriesIndex(float time, TFloatArrayList timelist, float threshold) { + for (int i = 0; i < timelist.size(); i++) { + float checktime = timelist.get(i); + float diff = Math.abs(checktime - time); + if (diff <= threshold) { + // Just one more try to get better fit + if (i + 1 < timelist.size() && Math.abs(checktime - time) < diff) + { + return i + 1; + } + return i; + } + if (checktime > time + threshold) { + return -1; + } + } + return -1; + } + + // Checks if it's withing threshold on both + and - side + boolean isNearATime(float time, TFloatArrayList timelist, float threshold) { + for (int i = 0; i < timelist.size(); i++) { + float checktime = timelist.get(i); + if (Math.abs(checktime - time) < threshold) { + return true; + } + if (checktime > time + threshold) { + return false; + } + } + return false; + } + + boolean sustainedFFT(float startTime, float len, float granularity, float timePerFFT, TFloatArrayList FFTMaxes, + TFloatArrayList FFTAvg, float aboveAvg, float averageMultiplier) { + int endIndex = (int) Math.floor((startTime + len) / timePerFFT); + if (endIndex >= FFTMaxes.size()) + return false; + int wiggleRoom = Math.round(0.1f * len / timePerFFT); + int startIndex = (int) Math.floor(startTime / timePerFFT); + int pastGranu = (int) Math.floor((startTime + granularity) / timePerFFT); + boolean startThresholdReached = false; + for (int i = startIndex; i <= endIndex; i++) { + float amt = FFTMaxes.getQuick(i); + float avg = FFTAvg.getQuick(i) * averageMultiplier; + if (i <= pastGranu) { + startThresholdReached |= amt >= avg + aboveAvg; + } else { + if (startThresholdReached == false) + return false; + if (amt < avg) { + wiggleRoom--; + if (wiggleRoom <= 0) + return false; + } + } + } + return true; + } + + public ArrayList> GetEvents(int stepGranularity, float timePerBeat, float timeOffset, + float totalTime, TFloatArrayList FFTAverages, TFloatArrayList FFTMaxes, TFloatArrayList volumes, float timePerFFT, + TFloatArrayList[] fewTimes, float sustainThresholdFactor, float granularityModifier, float preciseGranularityModifier) { + ArrayList> NoteEvents = new ArrayList<>(); + int timeIndex = 0; + float timeGranularity = timePerBeat / stepGranularity; + float standardPrecision = timeGranularity * granularityModifier; + float highPrecision = standardPrecision * preciseGranularityModifier; + float ultraPrecision = highPrecision / 2f; + + long sustains = 0; + long kicks = 0; + long snares = 0; + long beat = 0; + long hats = 0; + long silences = 0; + long beats = 0; + + for(float t = timeOffset; t <= totalTime; t += timeGranularity) { + boolean isQuarterBeat = false; + boolean isHalfBeat = false; + if (stepGranularity == 2) + { + isHalfBeat = timeIndex % 2 == 1; + } + else if (stepGranularity == 4) + { + isQuarterBeat = (timeIndex % 4 == 1 || timeIndex % 4 == 3); + isHalfBeat = timeIndex % 2 == 2; + } + boolean nearKick = false; + boolean nearHat = false; + boolean nearSnare = false; + boolean nearEnergy = false; + boolean silence = true; + if( t > 0f ) { + int idx = (int)Math.floor((t * FFTAverages.size()) / totalTime); + float fftmax = FFTMaxes.getQuick(idx); + float fftavg = FFTAverages.getQuick(idx); + boolean sustained = sustainedFFT(t, 0.75f, timeGranularity, timePerFFT, FFTMaxes, FFTAverages, sustainThresholdFactor, sustainThresholdFactor * 2); + + // OLD + // nearKick = isNearATime(t, fewTimes[SoundParameter.KICKS.value()], checkWindow); + // nearSnare = isNearATime(t, fewTimes[SoundParameter.SNARE.value()], checkWindow); + // nearEnergy = isNearATime(t, fewTimes[SoundParameter.BEAT.value()], checkWindow); + // // nearHat = isNearATime(t, fewTimes[SoundParameter.HAT.value()], checkWindow); + + // new + nearKick = isNearATime(t, fewTimes[SoundParameter.KICKS.value()], standardPrecision) && !isNearATime(t - highPrecision, fewTimes[SoundParameter.KICKS.value()], ultraPrecision); + nearSnare = isNearATime(t, fewTimes[SoundParameter.SNARE.value()], standardPrecision) && !isNearATime(t - highPrecision, fewTimes[SoundParameter.SNARE.value()], ultraPrecision); + nearEnergy = isNearATime(t, fewTimes[SoundParameter.BEAT.value()], standardPrecision) && !isNearATime(t - highPrecision, fewTimes[SoundParameter.BEAT.value()], ultraPrecision); + // nearHat = isNearATime(t, fewTimes[SoundParameter.HAT.value()], standardPrecision) && !isNearATime(t - highPrecision, fewTimes[SoundParameter.HAT.value()], ultraPrecision); + Map events = new HashMap<>(); + events.put(SoundParameter.KICKS, nearKick); + events.put(SoundParameter.SNARE, nearSnare); + events.put(SoundParameter.BEAT, nearEnergy); + // events.put(SoundParameter.HAT, nearHat); + events.put(SoundParameter.SUSTAINED, sustained); + // Some heuristic, best effort detection + silence = (fftmax + fftavg) < 0.2f; + events.put(SoundParameter.SILENCE, silence); + events.put(SoundParameter.HALF_BEAT, nearEnergy && isHalfBeat); + events.put(SoundParameter.QUARTER_BEAT, nearEnergy && isQuarterBeat); + events.put(SoundParameter.NOTHING, false); + events.put(SoundParameter.FFT_MAX, fftmax); + events.put(SoundParameter.FFT_AVG, fftavg); + events.put(SoundParameter.VOLUME, volumes.getQuick(idx)); + + if (sustained) sustains++; + if (nearSnare) snares++; + if (nearKick) kicks++; + if (nearHat) hats++; + if (silence) silences++; + if (nearEnergy) beats++; + NoteEvents.add(events); + } + else + { + Map events = new HashMap<>(); + events.put(SoundParameter.KICKS, false); + events.put(SoundParameter.SNARE, false); + events.put(SoundParameter.BEAT, false); + events.put(SoundParameter.SUSTAINED, false); + events.put(SoundParameter.SILENCE, true); + events.put(SoundParameter.HALF_BEAT, false); + events.put(SoundParameter.QUARTER_BEAT, false); + // events.put(SoundParameter.HAT, false); + events.put(SoundParameter.NOTHING, true); + events.put(SoundParameter.FFT_MAX, 0f); + events.put(SoundParameter.FFT_AVG, 0f); + events.put(SoundParameter.VOLUME, 0f); + NoteEvents.add(events); + } + timeIndex++; + } + + // System.out.println("SustainFactor: " + sustainFactor); + System.out.println("Sustains: " + sustains + ", Kicks: " + kicks + ", Snares: " + snares + ", Beat: " + beats + ", Silence: " + silences); + + return NoteEvents; + } +} diff --git a/src/autostepper/smfile/SmFileParser.java b/src/autostepper/smfile/SmFileParser.java new file mode 100644 index 0000000..fe789ab --- /dev/null +++ b/src/autostepper/smfile/SmFileParser.java @@ -0,0 +1,102 @@ +package autostepper.smfile; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.stream.Collectors; + +import autostepper.moveassigners.CStepAssigner; + +public class SmFileParser +{ + public static ArrayList parseFile(String filePath) { + ArrayList> allArrowLines = new ArrayList<>(); + + try (BufferedReader br = new BufferedReader(new FileReader(filePath))) { + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + ArrayList arrowLine = new ArrayList<>(); + for (char c : line.toCharArray()) { + arrowLine.add(c); + } + allArrowLines.add(arrowLine); + } + } catch (IOException e) { + e.printStackTrace(); + } + + return parseLines(allArrowLines); + } + + public static ArrayList parseLines(ArrayList> arrowLines) + { + ArrayList result = new ArrayList<>(); + boolean startCounting = false; + for (ArrayList arrayList : arrowLines) + { + StringBuilder sb = new StringBuilder(); + for (Character v : arrayList) { + sb.append(v); + } + String line = sb.toString(); + + // Only process lines of length 4 + if (line.length() == 4) { + if (!startCounting) { + // Skip lines until first non "0000" + if (!line.equals("0000")) { + startCounting = true; + // Count occurrences in this first non-"0000" line + result.add(countOnesOrTwos(line)); + } + } else { + // Count occurrences in subsequent lines + result.add(countOnesOrTwos(line)); + } + } + } + + return result; + } + + private static int countOnesOrTwos(String line) { + int count = 0; + for (char c : line.toCharArray()) { + if (c == '1' || c == '2') count++; + } + return count; + } + + public static String EncodeArrowLines(ArrayList> AllarrowLines, int stepGranularity) + { + // ok, put together AllNotes + int commaSeperatorReset = 4 * stepGranularity; + String AllNotes = ""; + int commaSeperator = commaSeperatorReset; + for (ArrayList arrayList : AllarrowLines) { + String result = arrayList.stream() + .map(c -> c == 'W' ? '0' : c) // to replace custom "HOLDING" to empty + .map(String::valueOf) + .collect(Collectors.joining()); + AllNotes += result + "\n"; + commaSeperator--; + if( commaSeperator == 0 ) { + AllNotes += ",\n"; + commaSeperator = commaSeperatorReset; + } + } + // fill out the last empties + while( commaSeperator > 0 ) { + AllNotes += "3333"; + commaSeperator--; + if( commaSeperator > 0 ) AllNotes += "\n"; + } + int _stepCount = AllNotes.length() - AllNotes.replace(Character.toString(CStepAssigner.STEP), "").length(); + int _holdCount = AllNotes.length() - AllNotes.replace(Character.toString(CStepAssigner.HOLD), "").length(); + int _mineCount = AllNotes.length() - AllNotes.replace(Character.toString(CStepAssigner.MINE), "").length(); + System.out.println("New algorithm. Steps: " + _stepCount + ", Holds: " + _holdCount); + return AllNotes; + } +} diff --git a/src/autostepper/soundprocessing/CExperimentalSoundProcessor.java b/src/autostepper/soundprocessing/CExperimentalSoundProcessor.java new file mode 100644 index 0000000..fb02b86 --- /dev/null +++ b/src/autostepper/soundprocessing/CExperimentalSoundProcessor.java @@ -0,0 +1,128 @@ +package autostepper.soundprocessing; + +import java.io.File; + +import autostepper.AutoStepper; +import autostepper.genetic.AlgorithmParameter; +import autostepper.misc.Averages; +import autostepper.misc.Utils; +import autostepper.vibejudges.SoundParameter; +import ddf.minim.AudioSample; +import ddf.minim.Minim; +import ddf.minim.MultiChannelBuffer; +import ddf.minim.analysis.BeatDetect; +import ddf.minim.analysis.FFT; +import ddf.minim.spi.AudioRecordingStream; +import gnu.trove.list.array.TFloatArrayList; + +public class CExperimentalSoundProcessor +{ + public static float SAMPLE_REDUCTION_RATIO = 1000f; + public static float MIN_BPM = 70f; + + private float bpm; + private float startTime = 0f; + private float timePerBeat = 0f; + private int lowerFrequencyKickThreshold; + private int higherFrequencyKickThreshold; + private int kickBandsThreshold; + private int lowerFrequencySnareThreshold; + private int higherFrequencySnareThreshold; + private int snareBandsThreshold; + private int lowerFrequencyHatThreshold; + private int higherFrequencyHatThreshold; + private int hatBandsThreshold; + + public CExperimentalSoundProcessor(TFloatArrayList params) { + lowerFrequencyKickThreshold = Math.round(params.get(AlgorithmParameter.KICK_LOW_FREQ.value())); + higherFrequencyKickThreshold = Math.round(params.get(AlgorithmParameter.KICK_HIGH_FREQ.value())); + kickBandsThreshold = Math.round(params.get(AlgorithmParameter.KICK_BAND_FREQ.value())); + + lowerFrequencySnareThreshold = Math.round(params.get(AlgorithmParameter.SNARE_LOW_FREQ.value())); + higherFrequencySnareThreshold = Math.round(params.get(AlgorithmParameter.SNARE_HIGH_FREQ.value())); + snareBandsThreshold = Math.round(params.get(AlgorithmParameter.SNARE_BAND_FREQ.value())); + + lowerFrequencyHatThreshold = Math.round(params.get(AlgorithmParameter.HAT_LOW_FREQ.value())); + higherFrequencyHatThreshold = Math.round(params.get(AlgorithmParameter.HAT_HIGH_FREQ.value())); + hatBandsThreshold = Math.round(params.get(AlgorithmParameter.HAT_BAND_FREQ.value())); + } + + public TFloatArrayList[] ProcessMusic(Song song, TFloatArrayList params) + { + // collected song data + final TFloatArrayList[] manyTimes = new TFloatArrayList[4]; + final TFloatArrayList[] fewTimes = new TFloatArrayList[4]; + + float timePerSample = song.getTimePerSample(); + + FrequencyConfig kicksConfig = new FrequencyConfig( + Math.round(params.get(AlgorithmParameter.KICK_LOW_FREQ.value())), + Math.round(params.get(AlgorithmParameter.KICK_HIGH_FREQ.value())), + Math.round(params.get(AlgorithmParameter.KICK_BAND_FREQ.value()))); + FrequencyConfig snaresConfig = new FrequencyConfig( + Math.round(params.get(AlgorithmParameter.SNARE_LOW_FREQ.value())), + Math.round(params.get(AlgorithmParameter.SNARE_HIGH_FREQ.value())), + Math.round(params.get(AlgorithmParameter.SNARE_BAND_FREQ.value()))); + FrequencyConfig hatsConfig = new FrequencyConfig( + Math.round(params.get(AlgorithmParameter.HAT_LOW_FREQ.value())), + Math.round(params.get(AlgorithmParameter.HAT_HIGH_FREQ.value())), + Math.round(params.get(AlgorithmParameter.HAT_BAND_FREQ.value()))); + song.processPercussions(kicksConfig, snaresConfig, hatsConfig, fewTimes, manyTimes); + + // calculate differences between percussive elements, + // then find the most common differences among all + // use this to calculate BPM + TFloatArrayList common = new TFloatArrayList(); + float doubleSpeed = 60f / (Song.MAX_BPM * 2f); + for (int i = 0; i < fewTimes.length; i++) { + AddCommonBPMs(common, fewTimes[i], doubleSpeed, timePerSample * 1.5f); + AddCommonBPMs(common, manyTimes[i], doubleSpeed, timePerSample * 1.5f); + } + if (common.isEmpty()) { + System.out.println("[--- FAILED: COULDN'T CALCULATE BPM ---]"); + throw new RuntimeException("Cannot determine BPM"); + } + bpm = Averages.getMostCommonFightlapa(common, 0.5f, true); + if (AutoStepper.SHOW_INFO) + { + System.out.println("[--- bpm: " + bpm + " ---]"); + } + + timePerBeat = 60f / bpm; + TFloatArrayList startTimes = new TFloatArrayList(); + for (int i = 0; i < fewTimes.length; i++) { + startTimes.add(Utils.getBestOffset(timePerBeat, fewTimes[i], 0.01f)); + startTimes.add(Utils.getBestOffset(timePerBeat, manyTimes[i], 0.01f)); + } + // give extra weight to fewKicks + float kickStartTime = Utils.getBestOffset(timePerBeat, fewTimes[SoundParameter.KICKS.value()], 0.01f); + startTimes.add(kickStartTime); + startTimes.add(kickStartTime); + startTime = -Averages.getMostCommonPhr00t(startTimes, 0.02f, false); + return fewTimes; + } + + void AddCommonBPMs(TFloatArrayList common, TFloatArrayList times, float doubleSpeed, float timePerSample) { + float commonBPM = 60f / Averages.getMostCommonFightlapa(Utils.calculateDifferences(times, doubleSpeed), timePerSample, true); + if (commonBPM > Song.MAX_BPM) { + common.add(commonBPM * 0.5f); + } else if (commonBPM < MIN_BPM / 2f) { + common.add(commonBPM * 4f); + } else if (commonBPM < MIN_BPM) { + common.add(commonBPM * 2f); + } else + common.add(commonBPM); + } + + public float GetBpm() { + return bpm; + } + + public float GetTimePerBeat() { + return timePerBeat; + } + + public float GetStartTime() { + return startTime; + } +} diff --git a/src/autostepper/soundprocessing/FrequencyConfig.java b/src/autostepper/soundprocessing/FrequencyConfig.java new file mode 100644 index 0000000..6b36ba8 --- /dev/null +++ b/src/autostepper/soundprocessing/FrequencyConfig.java @@ -0,0 +1,57 @@ +package autostepper.soundprocessing; + +public class FrequencyConfig { + int minimalFrequency; + int maximumFrequency; + int bandwithThreshold; + + public FrequencyConfig(int minimalFrequency, + int maximumFrequency, + int bandwithThreshold) + { + this.minimalFrequency = minimalFrequency; + this.maximumFrequency = maximumFrequency; + this.bandwithThreshold = bandwithThreshold; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + minimalFrequency; + result = prime * result + maximumFrequency; + result = prime * result + bandwithThreshold; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + FrequencyConfig other = (FrequencyConfig) obj; + if (minimalFrequency != other.minimalFrequency) + return false; + if (maximumFrequency != other.maximumFrequency) + return false; + if (bandwithThreshold != other.bandwithThreshold) + return false; + return true; + } + + public int getMinimalFrequency() { + return minimalFrequency; + } + + public int getMaximumFrequency() { + return maximumFrequency; + } + + public int getBandwithThreshold() { + return bandwithThreshold; + } + +} diff --git a/src/autostepper/soundprocessing/Song.java b/src/autostepper/soundprocessing/Song.java new file mode 100644 index 0000000..369f70a --- /dev/null +++ b/src/autostepper/soundprocessing/Song.java @@ -0,0 +1,316 @@ +package autostepper.soundprocessing; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import autostepper.AutoStepper; +import autostepper.misc.Utils; +import autostepper.vibejudges.SoundParameter; +import ddf.minim.AudioSample; +import ddf.minim.MultiChannelBuffer; +import ddf.minim.analysis.BeatDetect; +import ddf.minim.analysis.FFT; +import ddf.minim.spi.AudioRecordingStream; +import gnu.trove.list.array.TFloatArrayList; + +public class Song +{ + public static int FFT_SIZE = 512; + public static int BPM_SENSITIVITY_MS = 23; + public static float MAX_BPM = 170f; + private float songTime; + private String filename; + private float timePerSample; + private TFloatArrayList volume = new TFloatArrayList(); + private TFloatArrayList MidFFTAmount = new TFloatArrayList(); + private TFloatArrayList MidFFTMaxes = new TFloatArrayList(); + + final Map kicksLookupManyTimes = new HashMap<>(); + final Map kicksLookupFewTimes = new HashMap<>(); + final Map snaresLookupManyTimes = new HashMap<>(); + final Map snaresLookupFewTimes = new HashMap<>(); + final Map hatsLookupManyTimes = new HashMap<>(); + final Map hatsLookupFewTimes = new HashMap<>(); + final TFloatArrayList beatArrayFewTimes = new TFloatArrayList(); + final TFloatArrayList beatArrayManyTimes = new TFloatArrayList(); + + public Song(String filename) + { + this.filename = filename; + songTime = Utils.getSongTime(this.filename); + + AudioRecordingStream stream = AutoStepper.minimLib.loadFileStream(filename, FFT_SIZE, false); + + // tell it to "play" so we can read from it. + stream.play(); + + // create the buffer we use for reading from the stream + MultiChannelBuffer buffer = new MultiChannelBuffer(FFT_SIZE, stream.getFormat().getChannels()); + + // figure out how many samples are in the stream so we can allocate the correct + // number of spectra + int totalSamples = (int) (songTime * stream.getFormat().getSampleRate()); + timePerSample = FFT_SIZE / stream.getFormat().getSampleRate(); + + // now we'll analyze the samples in chunks + int totalChunks = (totalSamples / FFT_SIZE) + 1; + + float largestAvg = 0f, largestMax = 0f; + FFT fft = new FFT(FFT_SIZE, stream.getFormat().getSampleRate()); + int lowFreq = fft.freqToIndex(300f); + int highFreq = fft.freqToIndex(3000f); + + float maxVolume = 0f; + for (int chunkIdx = 0; chunkIdx < totalChunks; ++chunkIdx) + { + stream.read(buffer); + float[] samples = buffer.getChannel(0); + + fft.forward(samples); + // fft processing + float avg = fft.calcAvg(300f, 3000f); + float max = 0f; + for (int b = lowFreq; b <= highFreq; b++) { + float bandamp = fft.getBand(b); + if (bandamp > max) + max = bandamp; + } + if (max > largestMax) + largestMax = max; + if (avg > largestAvg) + largestAvg = avg; + MidFFTAmount.add(avg); + MidFFTMaxes.add(max); + + float rms = 0f; + for (float s : samples) { + rms += s * s; + } + float currVolume = (float) Math.sqrt(rms / samples.length); + volume.add(currVolume); + if (currVolume > maxVolume) + { + maxVolume = currVolume; + } + } + + float scaleBy = 1f / largestAvg; + float scaleMaxBy = 1f / largestMax; + float scaleVolume = 1f / maxVolume; + for (int i = 0; i < volume.size(); i++) { + volume.replace(i, volume.get(i) * scaleVolume); + MidFFTAmount.replace(i, MidFFTAmount.get(i) * scaleBy); + MidFFTMaxes.replace(i, MidFFTMaxes.get(i) * scaleMaxBy); + } + } + + /** + * This can be used for preprocessing, so that when revisiting sound parsing maybe we already calculated such results + */ + public void processPercussions(FrequencyConfig kickConfig, FrequencyConfig snareConfig, FrequencyConfig hatConfig, TFloatArrayList[] fewTimes, TFloatArrayList[] manyTimes) + { + long timer = System.currentTimeMillis(); + for (int i = 0; i < fewTimes.length; i++) { + if (fewTimes[i] == null) + fewTimes[i] = new TFloatArrayList(); + if (manyTimes[i] == null) + manyTimes[i] = new TFloatArrayList(); + fewTimes[i].clear(); + manyTimes[i].clear(); + } + + boolean kicksDone = false; + boolean snaresDone = false; + boolean hatsDone = false; + boolean beatsDone = false; + if (kicksLookupManyTimes.containsKey(kickConfig)) + { + manyTimes[SoundParameter.KICKS.value()].addAll(kicksLookupManyTimes.get(kickConfig)); + fewTimes[SoundParameter.KICKS.value()].addAll(kicksLookupFewTimes.get(kickConfig)); + kicksDone = true; + } + if (snaresLookupFewTimes.containsKey(snareConfig)) + { + manyTimes[SoundParameter.SNARE.value()].addAll(snaresLookupManyTimes.get(snareConfig)); + fewTimes[SoundParameter.SNARE.value()].addAll(snaresLookupFewTimes.get(snareConfig)); + snaresDone = true; + } + if (hatsLookupFewTimes.containsKey(hatConfig)) + { + manyTimes[SoundParameter.HAT.value()].addAll(hatsLookupManyTimes.get(hatConfig)); + fewTimes[SoundParameter.HAT.value()].addAll(hatsLookupFewTimes.get(hatConfig)); + hatsDone = true; + } + if (!beatArrayFewTimes.isEmpty()) + { + fewTimes[SoundParameter.BEAT.value()].addAll(beatArrayFewTimes); + manyTimes[SoundParameter.BEAT.value()].addAll(beatArrayManyTimes); + beatsDone = true; + } + + if (kicksDone && snaresDone && hatsDone && beatsDone) + { + if (AutoStepper.DEBUG_TIMINGS) + { + System.out.println("DONE FROM CACHE!" + (System.currentTimeMillis() - timer) / 1000f + "s"); + } + return; // Done from cache! + } + + AudioRecordingStream stream = AutoStepper.minimLib.loadFileStream(getFilename(), Song.FFT_SIZE, false); + + // tell it to "play" so we can read from it. + stream.play(); + + // create the fft/beatdetect objects we'll use for analysis + BeatDetect beatDetectFrequencyHighSensitivity = new BeatDetect(Song.FFT_SIZE, stream.getFormat().getSampleRate()); + BeatDetect beatDetectFrequencyLowSensitivity = new BeatDetect(Song.FFT_SIZE, stream.getFormat().getSampleRate()); + BeatDetect beatDetectSoundHighSensitivity = new BeatDetect(stream.getFormat().getSampleRate()); + BeatDetect beatDetectSoundLowSensitivity = new BeatDetect(stream.getFormat().getSampleRate()); + beatDetectFrequencyHighSensitivity.setSensitivity(BPM_SENSITIVITY_MS); + beatDetectSoundHighSensitivity.setSensitivity(BPM_SENSITIVITY_MS); + beatDetectFrequencyLowSensitivity.setSensitivity(Math.round(60000f / (float) MAX_BPM)); + beatDetectSoundLowSensitivity.setSensitivity(Math.round(60000f / (float) MAX_BPM)); + + // create the buffer we use for reading from the stream + MultiChannelBuffer buffer = new MultiChannelBuffer(Song.FFT_SIZE, stream.getFormat().getChannels()); + + // figure out how many samples are in the stream so we can allocate the correct + // number of spectra + int totalSamples = (int) (getSongTime() * stream.getFormat().getSampleRate()); + + // now we'll analyze the samples in chunks + int totalChunks = (totalSamples / Song.FFT_SIZE) + 1; + + + float timePerSample = getTimePerSample(); + float time = 0; + for (int chunkIdx = 0; chunkIdx < totalChunks; ++chunkIdx) + { + stream.read(buffer); + float[] data = buffer.getChannel(0); + // now analyze the left channel + beatDetectFrequencyHighSensitivity.detect(data); + beatDetectSoundHighSensitivity.detect(data); + beatDetectFrequencyLowSensitivity.detect(data); + beatDetectSoundLowSensitivity.detect(data); + + // store basic percussion times + if (!kicksDone) + { + if (beatDetectFrequencyHighSensitivity.isRange(kickConfig.getMinimalFrequency(), kickConfig.getMaximumFrequency(), kickConfig.getBandwithThreshold())) + manyTimes[SoundParameter.KICKS.value()].add(time); + if (beatDetectFrequencyLowSensitivity.isRange(kickConfig.getMinimalFrequency(), kickConfig.getMaximumFrequency(), kickConfig.getBandwithThreshold())) + fewTimes[SoundParameter.KICKS.value()].add(time); + } + + if (!snaresDone) + { + if (beatDetectFrequencyHighSensitivity.isRange(snareConfig.getMinimalFrequency(), snareConfig.getMaximumFrequency(), snareConfig.getBandwithThreshold())) + manyTimes[SoundParameter.SNARE.value()].add(time); + if (beatDetectFrequencyLowSensitivity.isRange(snareConfig.getMinimalFrequency(), snareConfig.getMaximumFrequency(), snareConfig.getBandwithThreshold())) + fewTimes[SoundParameter.SNARE.value()].add(time); + } + + if (!hatsDone) + { + if (beatDetectFrequencyHighSensitivity.isRange(hatConfig.getMinimalFrequency(), hatConfig.getMaximumFrequency(), hatConfig.getBandwithThreshold())) + manyTimes[SoundParameter.HAT.value()].add(time); + if (beatDetectFrequencyLowSensitivity.isRange(hatConfig.getMinimalFrequency(), hatConfig.getMaximumFrequency(), hatConfig.getBandwithThreshold())) + fewTimes[SoundParameter.HAT.value()].add(time); + } + + + if (!beatsDone) + { + if (beatDetectSoundLowSensitivity.isOnset()) + fewTimes[SoundParameter.BEAT.value()].add(time); + if (beatDetectSoundHighSensitivity.isOnset()) + manyTimes[SoundParameter.BEAT.value()].add(time); + } + + // if (beatDetectFrequencyHighSensitivity.isKick()) + // manyTimes[SoundParameter.KICKS.value()].add(time); + // if (beatDetectFrequencyHighSensitivity.isHat()) + // manyTimes[SoundParameter.HAT.value()].add(time); + // if (beatDetectFrequencyHighSensitivity.isSnare(false)) + // manyTimes[SoundParameter.SNARE.value()].add(time); + + // if (beatDetectFrequencyLowSensitivity.isKick()) + // fewTimes[SoundParameter.KICKS.value()].add(time); + // if (beatDetectFrequencyLowSensitivity.isHat()) + // fewTimes[SoundParameter.HAT.value()].add(time); + // if (beatDetectFrequencyLowSensitivity.isSnare(false)) + // fewTimes[SoundParameter.SNARE.value()].add(time); + + // if (beatDetectSoundLowSensitivity.isOnset()) + // fewTimes[SoundParameter.BEAT.value()].add(time); + + // if (beatDetectSoundHighSensitivity.isOnset()) + // manyTimes[SoundParameter.BEAT.value()].add(time); + time += timePerSample; + } + + // store basic percussion times + if (!kicksDone) + { + kicksLookupFewTimes.putIfAbsent(kickConfig, new TFloatArrayList()); + kicksLookupManyTimes.putIfAbsent(kickConfig, new TFloatArrayList()); + kicksLookupFewTimes.get(kickConfig).addAll(fewTimes[SoundParameter.KICKS.value()]); + kicksLookupManyTimes.get(kickConfig).addAll(manyTimes[SoundParameter.KICKS.value()]); + } + + if (!snaresDone) + { + snaresLookupFewTimes.putIfAbsent(snareConfig, new TFloatArrayList()); + snaresLookupManyTimes.putIfAbsent(snareConfig, new TFloatArrayList()); + snaresLookupFewTimes.get(snareConfig).addAll(fewTimes[SoundParameter.SNARE.value()]); + snaresLookupManyTimes.get(snareConfig).addAll(manyTimes[SoundParameter.SNARE.value()]); + } + + if (!hatsDone) + { + hatsLookupFewTimes.putIfAbsent(hatConfig, new TFloatArrayList()); + hatsLookupManyTimes.putIfAbsent(hatConfig, new TFloatArrayList()); + hatsLookupFewTimes.get(hatConfig).addAll(fewTimes[SoundParameter.HAT.value()]); + hatsLookupManyTimes.get(hatConfig).addAll(manyTimes[SoundParameter.HAT.value()]); + } + + + if (!beatsDone) + { + beatArrayFewTimes.addAll(fewTimes[SoundParameter.BEAT.value()]); + beatArrayManyTimes.addAll(manyTimes[SoundParameter.BEAT.value()]); + } + + if (AutoStepper.DEBUG_TIMINGS) + { + System.out.println("MAIN LOOP TIME: " + (System.currentTimeMillis() - timer) / 1000f + "s"); + } + } + + public float getSongTime() { + return songTime; + } + + public String getFilename() { + return filename; + } + + public float getTimePerSample() { + return timePerSample; + } + + public TFloatArrayList getVolume() { + return volume; + } + + public TFloatArrayList getMidFFTAmount() { + return MidFFTAmount; + } + + public TFloatArrayList getMidFFTMaxes() { + return MidFFTMaxes; + } +} diff --git a/src/autostepper/useractions/BPMOffset.java b/src/autostepper/useractions/BPMOffset.java new file mode 100644 index 0000000..2e9c027 --- /dev/null +++ b/src/autostepper/useractions/BPMOffset.java @@ -0,0 +1,3 @@ +package autostepper.useractions; + +public record BPMOffset(int BPM, float offset) {} \ No newline at end of file diff --git a/src/autostepper/useractions/UserActions.java b/src/autostepper/useractions/UserActions.java new file mode 100644 index 0000000..4dbbf07 --- /dev/null +++ b/src/autostepper/useractions/UserActions.java @@ -0,0 +1,51 @@ +package autostepper.useractions; + +import java.util.Scanner; + +import autostepper.AutoStepper; +import autostepper.misc.Utils; +import ddf.minim.AudioSample; +import gnu.trove.list.array.TFloatArrayList; + + +public class UserActions +{ + public static BPMOffset getTappedBPM(String filename) { + // now we load the whole song so we don't have to worry about streaming a + // variable mp3 with timing inaccuracies + System.out.println("Loading whole song for tapping..."); + AudioSample fullSong = AutoStepper.minimLib.loadSample(filename); + System.out.println( + "\n********************************************************************\n\nPress [ENTER] to start song, then press [ENTER] to tap to the beat.\nIt will complete after 30 entries.\nDon't worry about hitting the first beat, just start anytime.\n\n********************************************************************"); + TFloatArrayList positions = new TFloatArrayList(); + Scanner in = new Scanner(System.in); + try { + in.nextLine(); + } catch (Exception e) { + } + // get the most accurate start time as possible + long nano = System.nanoTime(); + fullSong.trigger(); + nano = (System.nanoTime() + nano) / 2; + try { + for (int i = 0; i < 30; i++) { + in.nextLine(); + // get two playtime values & average them together for accuracy + long now = System.nanoTime(); + // calculate the time difference + // we note a consistent 0.11 second delay in input to song here + double time = (double) ((now - nano) / 1000000000.0) + AutoStepper.TAPSYNC; + positions.add((float) time); + System.out.println("#" + positions.size() + "/30: " + time + "s"); + } + } catch (Exception e) { + } + fullSong.stop(); + fullSong.close(); + float avg = ((positions.getQuick(positions.size() - 1) - positions.getQuick(0)) / (positions.size() - 1)); + int BPM = (int) Math.floor(60f / avg); + float timePerBeat = 60f / BPM; + float tappedOffset = -Utils.getBestOffset(timePerBeat, positions, 0.1f); + return new BPMOffset(BPM, tappedOffset); + } +} diff --git a/src/autostepper/vibejudges/DeafToBeatJudge.java b/src/autostepper/vibejudges/DeafToBeatJudge.java new file mode 100644 index 0000000..01e6cfa --- /dev/null +++ b/src/autostepper/vibejudges/DeafToBeatJudge.java @@ -0,0 +1,61 @@ +package autostepper.vibejudges; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class DeafToBeatJudge implements IVibeJudge { + + @Override + public ArrayList> GetVibes(ArrayList> NoteEvents) + { + ArrayList> NoteVibes = new ArrayList<>(); + + for (Map map : NoteEvents) { + int vibePower = 0; + int sustainPower = 0; + if (!(boolean)map.get(SoundParameter.NOTHING)) + { + if ((boolean)map.get(SoundParameter.KICKS)) + { + // Some standard sound or something to sustain deserves at least some vibe + vibePower++; + } + + if ((boolean)map.get(SoundParameter.SNARE)) + { + // Some standard sound or something to sustain deserves at least some vibe + vibePower++; + } + + if ((float)map.get(SoundParameter.VOLUME) > 0.8f) + { + // That's some loud sound right there, give it some vibe + vibePower++; + } + + if ( (boolean)map.get(SoundParameter.SUSTAINED) ) { + sustainPower++; + if ( (boolean)map.get(SoundParameter.BEAT) || (boolean)map.get(SoundParameter.HALF_BEAT)) + { + // Extra boost if that's the beat + sustainPower++; + } + } else if( (boolean)map.get(SoundParameter.SILENCE) ) { + sustainPower--; + } + } + Map noteVibe = new HashMap<>(); + noteVibe.put(VibeScore.POWER, vibePower); + noteVibe.put(VibeScore.SUSTAIN, sustainPower); + NoteVibes.add(noteVibe); + } + return NoteVibes; + } + + @Override + public String WhatsYourNameMrJudge() { + return "I CANNOT HEAR YOU"; + } + +} diff --git a/src/autostepper/vibejudges/ExcitedByEverythingJudge.java b/src/autostepper/vibejudges/ExcitedByEverythingJudge.java new file mode 100644 index 0000000..44bbefe --- /dev/null +++ b/src/autostepper/vibejudges/ExcitedByEverythingJudge.java @@ -0,0 +1,85 @@ +package autostepper.vibejudges; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class ExcitedByEverythingJudge implements IVibeJudge { + + // for slower tracks where not much is happening + + @Override + public ArrayList> GetVibes(ArrayList> NoteEvents) + { + ArrayList> NoteVibes = new ArrayList<>(); + + for (Map map : NoteEvents) { + int vibePower = 0; + int sustainPower = 0; + if (!(boolean)map.get(SoundParameter.NOTHING)) + { + if ((boolean)map.get(SoundParameter.KICKS)) + { + // Some standard sound or something to sustain deserves at least some vibe + vibePower++; + } + + if ((boolean)map.get(SoundParameter.SNARE)) + { + // Some standard sound or something to sustain deserves at least some vibe + vibePower+=2; + } + + if ((boolean)map.get(SoundParameter.BEAT)) + { + // Some standard sound or something to sustain deserves at least some vibe + vibePower++; + } + + if ((boolean)map.get(SoundParameter.BEAT) && (boolean)map.get(SoundParameter.KICKS) && !(boolean)map.get(SoundParameter.HALF_BEAT)) + { + // If it's kicks, on beat, without half-beat, that sounds like a good candidate for jump, that sounds like good vibe + vibePower+=2; + } + + if ((float)map.get(SoundParameter.VOLUME) > 0.4f) + { + // That's some loud sound right there, give it some vibe + vibePower++; + } + if ((float)map.get(SoundParameter.VOLUME) > 0.8f) + { + // That's some loud sound right there, give it some more vibe + vibePower++; + } + + if ((float)map.get(SoundParameter.VOLUME) > 0.9f) + { + // That's some loud sound right there, give it some more vibe + vibePower++; + } + + if ( (boolean)map.get(SoundParameter.SUSTAINED) ) { + sustainPower++; + if ( (boolean)map.get(SoundParameter.BEAT) || (boolean)map.get(SoundParameter.HALF_BEAT)) + { + // Extra boost if that's the beat + sustainPower++; + } + } else if( (boolean)map.get(SoundParameter.SILENCE) ) { + sustainPower--; + } + } + Map noteVibe = new HashMap<>(); + noteVibe.put(VibeScore.POWER, vibePower); + noteVibe.put(VibeScore.SUSTAIN, sustainPower); + NoteVibes.add(noteVibe); + } + return NoteVibes; + } + + @Override + public String WhatsYourNameMrJudge() { + return "I'm so excited I forgot my name."; + } +} diff --git a/src/autostepper/vibejudges/IVibeJudge.java b/src/autostepper/vibejudges/IVibeJudge.java new file mode 100644 index 0000000..83aef8f --- /dev/null +++ b/src/autostepper/vibejudges/IVibeJudge.java @@ -0,0 +1,11 @@ +package autostepper.vibejudges; + +import java.util.ArrayList; +import java.util.Map; + +public interface IVibeJudge { + + public ArrayList> GetVibes(ArrayList> NoteEvents); + + public String WhatsYourNameMrJudge(); +} diff --git a/src/autostepper/vibejudges/ParametrizedJudge.java b/src/autostepper/vibejudges/ParametrizedJudge.java new file mode 100644 index 0000000..729fc78 --- /dev/null +++ b/src/autostepper/vibejudges/ParametrizedJudge.java @@ -0,0 +1,101 @@ +package autostepper.vibejudges; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class ParametrizedJudge implements IVibeJudge { + + private float firstVolumeThreshold; + private float secondVolumeThreshold; + private float maxFftThreshold; + + public ParametrizedJudge(float firstVolumeThreshold, float secondVolumeThreshold, float maxFftThreshold) + { + this.firstVolumeThreshold = firstVolumeThreshold; + this.secondVolumeThreshold = secondVolumeThreshold; + this.maxFftThreshold = maxFftThreshold; + } + + @Override + public ArrayList> GetVibes(ArrayList> NoteEvents) + { + ArrayList> NoteVibes = new ArrayList<>(); + + for (Map map : NoteEvents) { + int vibePower = 0; + int sustainPower = 0; + if (!(boolean)map.get(SoundParameter.NOTHING)) + { + if ((boolean)map.get(SoundParameter.KICKS)) + { + // Some standard sound or something to sustain deserves at least some vibe + vibePower++; + } + + if ((boolean)map.get(SoundParameter.SNARE)) + { + // Some standard sound or something to sustain deserves at least some vibe + vibePower+=2; + } + + if ((boolean)map.get(SoundParameter.BEAT)) + { + // Some standard sound or something to sustain deserves at least some vibe + vibePower++; + } + + if ((boolean)map.get(SoundParameter.BEAT) && (boolean)map.get(SoundParameter.KICKS) && !(boolean)map.get(SoundParameter.HALF_BEAT)) + { + // If it's kicks, on beat, without half-beat, that sounds like a good candidate for jump, that sounds like good vibe + vibePower+=2; + } + + if ((boolean)map.get(SoundParameter.BEAT) && (boolean)map.get(SoundParameter.QUARTER_BEAT)) + { + // To not treat equally quarter beat, it should have less vibe + vibePower--; + } + + if ((float)map.get(SoundParameter.VOLUME) > firstVolumeThreshold) + { + // That's some loud sound right there, give it some vibe + vibePower++; + } + + if ((float)map.get(SoundParameter.VOLUME) > secondVolumeThreshold) + { + // That's some loud sound right there, give it some more vibe + vibePower++; + } + + if ((float)map.get(SoundParameter.FFT_MAX) > maxFftThreshold) + { + // That's some loud sound right there, give it some more vibe + vibePower++; + } + + if ( (boolean)map.get(SoundParameter.SUSTAINED) ) { + sustainPower++; + if ( (boolean)map.get(SoundParameter.BEAT) || (boolean)map.get(SoundParameter.HALF_BEAT)) + { + // Extra boost if that's the beat + sustainPower++; + } + } else if( (boolean)map.get(SoundParameter.SILENCE) ) { + sustainPower--; + } + } + Map noteVibe = new HashMap<>(); + noteVibe.put(VibeScore.POWER, vibePower); + noteVibe.put(VibeScore.SUSTAIN, sustainPower); + NoteVibes.add(noteVibe); + } + return NoteVibes; + } + + @Override + public String WhatsYourNameMrJudge() { + return "I'm so excited I forgot my name."; + } +} diff --git a/src/autostepper/vibejudges/PopJudge.java b/src/autostepper/vibejudges/PopJudge.java new file mode 100644 index 0000000..e905152 --- /dev/null +++ b/src/autostepper/vibejudges/PopJudge.java @@ -0,0 +1,73 @@ +package autostepper.vibejudges; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class PopJudge implements IVibeJudge { + + @Override + public ArrayList> GetVibes(ArrayList> NoteEvents) + { + ArrayList> NoteVibes = new ArrayList<>(); + + for (Map map : NoteEvents) { + int vibePower = 0; + int sustainPower = 0; + if (!(boolean)map.get(SoundParameter.NOTHING)) + { + if ((boolean)map.get(SoundParameter.KICKS)) + { + // Some standard sound or something to sustain deserves at least some vibe + vibePower++; + } + + if ((boolean)map.get(SoundParameter.SNARE)) + { + // Some standard sound or something to sustain deserves at least some vibe + vibePower++; + } + + if ((boolean)map.get(SoundParameter.BEAT)) + { + // Some standard sound or something to sustain deserves at least some vibe + vibePower++; + } + + if ((boolean)map.get(SoundParameter.BEAT) && (boolean)map.get(SoundParameter.KICKS) && !(boolean)map.get(SoundParameter.HALF_BEAT)) + { + // If it's kicks, on beat, without half-beat, that sounds like a good candidate for jump, that sounds like good vibe + vibePower++; + } + + if ((float)map.get(SoundParameter.VOLUME) > 0.8f) + { + // That's some loud sound right there, give it some vibe + vibePower++; + } + + if ( (boolean)map.get(SoundParameter.SUSTAINED) ) { + sustainPower++; + if ( (boolean)map.get(SoundParameter.BEAT) || (boolean)map.get(SoundParameter.HALF_BEAT)) + { + // Extra boost if that's the beat + sustainPower++; + } + } else if( (boolean)map.get(SoundParameter.SILENCE) ) { + sustainPower--; + } + } + Map noteVibe = new HashMap<>(); + noteVibe.put(VibeScore.POWER, vibePower); + noteVibe.put(VibeScore.SUSTAIN, sustainPower); + NoteVibes.add(noteVibe); + } + return NoteVibes; + } + + @Override + public String WhatsYourNameMrJudge() { + return "My name is Jeff. 21st pop song finalists."; + } + +} diff --git a/src/autostepper/vibejudges/SoundParameter.java b/src/autostepper/vibejudges/SoundParameter.java new file mode 100644 index 0000000..165d78e --- /dev/null +++ b/src/autostepper/vibejudges/SoundParameter.java @@ -0,0 +1,26 @@ +package autostepper.vibejudges; + +public enum SoundParameter { + KICKS(0), + BEAT(1), + SNARE(2), + HAT(3), + SUSTAINED(4), + SILENCE(5), + NOTHING(6), + HALF_BEAT(7), + FFT_MAX(8), + FFT_AVG(9), + VOLUME(10), + QUARTER_BEAT(7); + + private final int value; + + SoundParameter(int value) { + this.value = value; + } + + public int value() { + return value; + } +} \ No newline at end of file diff --git a/src/autostepper/vibejudges/VibeScore.java b/src/autostepper/vibejudges/VibeScore.java new file mode 100644 index 0000000..6fb53ad --- /dev/null +++ b/src/autostepper/vibejudges/VibeScore.java @@ -0,0 +1,16 @@ +package autostepper.vibejudges; + +public enum VibeScore { + POWER(0), + SUSTAIN(1); + + private final int value; + + VibeScore(int value) { + this.value = value; + } + + public int value() { + return value; + } +} \ No newline at end of file diff --git a/src/ddf/minim/AudioInput.java b/src/ddf/minim/AudioInput.java index b59732c..e912025 100644 --- a/src/ddf/minim/AudioInput.java +++ b/src/ddf/minim/AudioInput.java @@ -115,6 +115,7 @@ public boolean isMonitoring() * @related isMonitoring ( ) * @related AudioInput */ + @SuppressWarnings("deprecation") public void enableMonitoring() { // make sure we don't make sound @@ -154,6 +155,7 @@ else if ( hasControl(GAIN) ) * @related AudioInput * */ + @SuppressWarnings("deprecation") public void disableMonitoring() { // make sure we don't make sound diff --git a/src/ddf/minim/AudioOutput.java b/src/ddf/minim/AudioOutput.java index cbf1a73..ccffcb0 100644 --- a/src/ddf/minim/AudioOutput.java +++ b/src/ddf/minim/AudioOutput.java @@ -68,6 +68,7 @@ * @example Basics/SynthesizeSound * @example Basics/SequenceSound */ +@SuppressWarnings("deprecation") public class AudioOutput extends AudioSource implements Polyphonic { // the synth attach our signals to diff --git a/src/ddf/minim/AudioPlayer.java b/src/ddf/minim/AudioPlayer.java index 72f5a32..35e51f4 100644 --- a/src/ddf/minim/AudioPlayer.java +++ b/src/ddf/minim/AudioPlayer.java @@ -148,9 +148,13 @@ public void rewind() */ public void loop(int num) { - // if we were paused, we need to grab the current state - // because calling loop totally resets it - if ( isPaused ) + // if we were paused, we need to grab the current state because calling loop totally resets it. + // Issue #72: if the recording is currently playing, we also need to do this, + // otherwise we start the loop over, which contradicts the above documentation. + // + // If this has never been paused before and the stream isn't playing, + // then people probably will expect the file to start playing from the loopStart, not from the beginning. + if ( isPaused || recording.isPlaying() ) { int pos = recording.getMillisecondPosition(); recording.loop( num ); @@ -233,8 +237,8 @@ public int position() * the beginning. This will not change the play state. If an error * occurs while trying to cue, the position will not change. * If you try to cue to a negative position or to a position - * that is greater than length(), the amount will be clamped - * to zero or length(). + * that is greater than a non-negative length(), + * the amount will be clamped to zero or length(). * * @shortdesc Sets the position to millis milliseconds from * the beginning. @@ -250,45 +254,47 @@ public int position() public void cue(int millis) { if (millis < 0) - { + { millis = 0; - } - else if (millis > length()) - { - millis = length(); - } + } + else + { + // only clamp millis to the length of the file if the length is known. + // otherwise we will try to skip what is asked and count on the underlying stream to handle it. + int len = recording.getMillisecondLength(); + if (len >= 0 && millis > len) + { + millis = len; + } + } recording.setMillisecondPosition(millis); } - /** - * Skips millis milliseconds from the current position. - * millis can be negative, which will make this skip backwards. - * If the skip amount would result in a negative position or a position that is greater than - * length(), the new position will be clamped to zero or - * length(). - * - * @shortdesc Skips millis milliseconds from the current position. - * - * @param millis - * int: how many milliseconds to skip, sign indicates direction - * - * @example AudioPlayer/skip - * - * @related AudioPlayer - */ + /** + * Skips millis milliseconds from the current position. + * millis can be negative, which will make this skip backwards. + * If the skip amount would result in a negative position or a position that is greater than + * a non-negative length(), the new position will be clamped to zero or length(). + * + * @shortdesc Skips millis milliseconds from the current position. + * + * @param millis + * int: how many milliseconds to skip, sign indicates direction + * + * @example AudioPlayer/skip + * + * @related AudioPlayer + */ public void skip(int millis) { int pos = position() + millis; - if (pos < 0) + if ( pos < 0 ) { pos = 0; } - else if (pos > length()) - { - pos = length(); - } - Minim.debug("AudioPlayer.skip: skipping " + millis + " milliseconds, new position is " + pos); - recording.setMillisecondPosition(pos); + + Minim.debug("AudioPlayer.skip: attempting to skip " + millis + " milliseconds, to position " + pos); + cue(pos); } /** @@ -336,22 +342,70 @@ public AudioMetaData getMetaData() } /** - * Sets the loop points used when looping. - * - * @param start - * int: the start of the loop in milliseconds - * @param stop - * int: the end of the loop in milliseconds + * Sets the beginning and end of the section to loop when looping. + * These should be between 0 and the length of the file. + * If end is larger than the length of the file, + * the end of the loop will be set to the end of the file. + * If the length of the file is unknown and is positive, + * it will be used directly. + * If end is negative, the end of the loop + * will be set to the end of the file. + * If begin is greater than end + * (unless end is negative), it will be clamped + * to one millisecond before end. + * + * @param begin + * int: the beginning of the loop in milliseconds + * @param end + * int: the end of the loop in milliseconds, or -1 to set it to the end of the file * * @example AudioPlayer/setLoopPoints * + * @related loop ( ) + * @related getLoopBegin ( ) + * @related getLoopEnd ( ) * @related AudioPlayer */ - public void setLoopPoints(int start, int stop) + public void setLoopPoints(int begin, int end) { - recording.setLoopPoints(start, stop); - + recording.setLoopPoints(begin, end); + } + + /** + * Gets the current millisecond position of the beginning of the looped section. + * + * @return + * int: the beginning of the looped section in milliseconds + * + * @example AudioPlayer/setLoopPoints + * + * @related setLoopPoints ( ) + * @related loop ( ) + * @related AudioPlayer + * + */ + public int getLoopBegin() + { + return recording.getLoopBegin(); } + + /** + * Gets the current millisecond position of the end of the looped section. + * This can be -1 if the length is unknown and setLoopPoints has never been called. + * + * @return + * int: the end of the looped section in milliseconds + * + * @example AudioPlayer/setLoopPoints + * + * @related setLoopPoints ( ) + * @related loop ( ) + * @related AudioPlayer + */ + public int getLoopEnd() + { + return recording.getLoopEnd(); + } /** * Release the resources associated with playing this file. diff --git a/src/ddf/minim/AudioRecorder.java b/src/ddf/minim/AudioRecorder.java index ecdf550..f6c88d9 100644 --- a/src/ddf/minim/AudioRecorder.java +++ b/src/ddf/minim/AudioRecorder.java @@ -161,7 +161,7 @@ public void setRecordSource(Recordable recordSource) public void setSampleRecorder(SampleRecorder recorder) { this.recorder.endRecord(); - this.recorder.save(); + this.recorder.save().close(); source.removeListener(this.recorder); source.addListener(recorder); this.recorder = recorder; diff --git a/src/ddf/minim/AudioSnippet.java b/src/ddf/minim/AudioSnippet.java index 5965f2c..aca3714 100644 --- a/src/ddf/minim/AudioSnippet.java +++ b/src/ddf/minim/AudioSnippet.java @@ -98,20 +98,32 @@ public int position() public void cue(int millis) { if (millis < 0) + { millis = 0; - if (millis > length()) - millis = length(); + } + else + { + // only clamp millis to the length of the file if the length is known. + // otherwise we will try to skip what is asked and count on the underlying stream to handle it. + int len = recording.getMillisecondLength(); + if (len >= 0 && millis > len) + { + millis = len; + } + } recording.setMillisecondPosition(millis); } public void skip(int millis) { int pos = position() + millis; - if (pos < 0) + if ( pos < 0 ) + { pos = 0; - else if (pos > length()) - pos = length(); - recording.setMillisecondPosition(pos); + } + + Minim.debug("AudioSnippet.skip: attempting to skip " + millis + " milliseconds, to position " + pos); + cue(pos); } public boolean isLooping() @@ -143,4 +155,14 @@ public void setLoopPoints(int start, int stop) { recording.setLoopPoints(start, stop); } + + public int getLoopBegin() + { + return recording.getLoopBegin(); + } + + public int getLoopEnd() + { + return recording.getLoopEnd(); + } } diff --git a/src/ddf/minim/AudioSource.java b/src/ddf/minim/AudioSource.java index 611a0a3..25a7b46 100644 --- a/src/ddf/minim/AudioSource.java +++ b/src/ddf/minim/AudioSource.java @@ -20,6 +20,7 @@ * @invisible * */ +@SuppressWarnings("deprecation") public class AudioSource extends Controller implements Effectable, Recordable { // the instance of Minim that created us, if one did. diff --git a/src/ddf/minim/BasicAudioOut.java b/src/ddf/minim/BasicAudioOut.java index 9437acd..13e150e 100644 --- a/src/ddf/minim/BasicAudioOut.java +++ b/src/ddf/minim/BasicAudioOut.java @@ -94,6 +94,7 @@ public int bufferSize() } + @SuppressWarnings("deprecation") public void setAudioSignal(AudioSignal signal) { //Minim.error( "BasicAudioOut does not support setting an AudioSignal." ); @@ -104,6 +105,7 @@ public void setAudioStream(AudioStream stream) this.stream = stream; } + @SuppressWarnings("deprecation") public void setAudioEffect(AudioEffect effect) { //Minim.error( "BasicAudiOut does not support setting an AudioEffect." ); diff --git a/src/ddf/minim/Effectable.java b/src/ddf/minim/Effectable.java index 9eeb496..ffd1256 100644 --- a/src/ddf/minim/Effectable.java +++ b/src/ddf/minim/Effectable.java @@ -28,6 +28,7 @@ * @invisible * */ +@Deprecated public interface Effectable { /** diff --git a/src/ddf/minim/Minim.java b/src/ddf/minim/Minim.java index be1244c..f37a520 100644 --- a/src/ddf/minim/Minim.java +++ b/src/ddf/minim/Minim.java @@ -81,6 +81,7 @@ * @author Damien Di Fede */ +@SuppressWarnings("deprecation") public class Minim { /** Specifies that you want a MONO AudioInput or AudioOutput */ @@ -204,9 +205,9 @@ public Minim( MinimServiceProvider implementation ) */ public static void error(String message) { - System.out.println( "=== Minim Error ===" ); - System.out.println( "=== " + message ); - System.out.println(); + // System.out.println( "=== Minim Error ===" ); + // System.out.println( "=== " + message ); + // System.out.println(); } /** @invisible @@ -297,6 +298,7 @@ public void stop() { s.close(); } + streams.clear(); // stop the implementation mimp.stop(); @@ -312,6 +314,19 @@ void removeSource( AudioSource s ) { sources.remove( s ); } + + void addStream( AudioStream s ) + { + if ( !streams.contains( s )) + { + streams.add( s ); + } + } + + void removeStream( AudioStream s) + { + streams.remove( s ); + } /** * When using the JavaSound implementation of Minim, this sets the JavaSound Mixer @@ -390,7 +405,10 @@ public AudioSample createSample(float[] sampleData, AudioFormat format) public AudioSample createSample( float[] sampleData, AudioFormat format, int bufferSize ) { AudioSample sample = mimp.getAudioSample( sampleData, format, bufferSize ); - addSource( sample ); + if ( sample != null ) + { + addSource( sample ); + } return sample; } @@ -432,7 +450,10 @@ public AudioSample createSample( float[] leftSampleData, float[] rightSampleData public AudioSample createSample(float[] leftSampleData, float[] rightSampleData, AudioFormat format, int bufferSize) { AudioSample sample = mimp.getAudioSample( leftSampleData, rightSampleData, format, bufferSize ); - addSource( sample ); + if ( sample != null ) + { + addSource( sample ); + } return sample; } @@ -473,7 +494,10 @@ public AudioSample loadSample(String filename) public AudioSample loadSample(String filename, int bufferSize) { AudioSample sample = mimp.getAudioSample( filename, bufferSize ); - addSource( sample ); + if ( sample != null ) + { + addSource( sample ); + } return sample; } @@ -596,8 +620,7 @@ public AudioPlayer loadFile(String filename, int bufferSize) public AudioRecordingStream loadFileStream(String filename, int bufferSize, boolean inMemory) { AudioRecordingStream stream = mimp.getAudioRecordingStream( filename, bufferSize, inMemory ); - streams.add( stream ); - return stream; + return stream == null ? null : new TrackedAudioRecordingStream( this, stream ); } /** @@ -634,7 +657,8 @@ public AudioMetaData loadMetaData(String filename) /** * Loads the requested file into a MultiChannelBuffer. The buffer's channel count - * and buffer size will be adjusted to match the file. + * and buffer size will be adjusted to match the file. Loading the file will fail + * if the length of the file cannot be determined. * * @shortdesc Loads the requested file into a MultiChannelBuffer. * @@ -655,62 +679,66 @@ public float loadFileIntoBuffer( String filename, MultiChannelBuffer outBuffer ) float sampleRate = 0; AudioRecordingStream stream = mimp.getAudioRecordingStream( filename, readBufferSize, false ); if ( stream != null ) - { - //stream.open(); - stream.play(); - sampleRate = stream.getFormat().getSampleRate(); - final int channelCount = stream.getFormat().getChannels(); - // for reading the file in, in chunks. - MultiChannelBuffer readBuffer = new MultiChannelBuffer( channelCount, readBufferSize ); - // make sure the out buffer is the correct size and type. - outBuffer.setChannelCount( channelCount ); + { // how many samples to read total long totalSampleCount = stream.getSampleFrameLength(); - if ( totalSampleCount == -1 ) + if ( totalSampleCount == -1 && stream.getMillisecondLength() != -1 ) { totalSampleCount = AudioUtils.millis2Frames( stream.getMillisecondLength(), stream.getFormat() ); } - debug( "Total sample count for " + filename + " is " + totalSampleCount ); - outBuffer.setBufferSize( (int)totalSampleCount ); - // now read in chunks. - long totalSamplesRead = 0; - while( totalSamplesRead < totalSampleCount ) + if ( totalSampleCount > 0 ) { - // is the remainder smaller than our buffer? - if ( totalSampleCount - totalSamplesRead < readBufferSize ) - { - readBuffer.setBufferSize( (int)(totalSampleCount - totalSamplesRead) ); - } + stream.play(); + sampleRate = stream.getFormat().getSampleRate(); + final int channelCount = stream.getFormat().getChannels(); + // for reading the file in, in chunks. + MultiChannelBuffer readBuffer = new MultiChannelBuffer( channelCount, readBufferSize ); + // make sure the out buffer is the correct size and type. + outBuffer.setChannelCount( channelCount ); + + debug( "Total sample count for " + filename + " is " + totalSampleCount ); + outBuffer.setBufferSize( (int)totalSampleCount ); - int samplesRead = stream.read( readBuffer ); - - if ( samplesRead == 0 ) + // now read in chunks. + long totalSamplesRead = 0; + while( totalSamplesRead < totalSampleCount ) { - debug( "loadSampleIntoBuffer: got 0 samples read" ); - break; + // is the remainder smaller than our buffer? + if ( totalSampleCount - totalSamplesRead < readBufferSize ) + { + readBuffer.setBufferSize( (int)(totalSampleCount - totalSamplesRead) ); + } + + int samplesRead = stream.read( readBuffer ); + + if ( samplesRead == 0 ) + { + debug( "loadSampleIntoBuffer: got 0 samples read" ); + break; + } + + // copy data from one buffer to the other. + for(int i = 0; i < channelCount; ++i) + { + // a faster way to do this would be nice. + for(int s = 0; s < samplesRead; ++s) + { + outBuffer.setSample( i, (int)totalSamplesRead+s, readBuffer.getSample( i, s ) ); + } + } + + totalSamplesRead += samplesRead; } - // copy data from one buffer to the other. - for(int i = 0; i < channelCount; ++i) + if ( totalSamplesRead != totalSampleCount ) { - // a faster way to do this would be nice. - for(int s = 0; s < samplesRead; ++s) - { - outBuffer.setSample( i, (int)totalSamplesRead+s, readBuffer.getSample( i, s ) ); - } + outBuffer.setBufferSize( (int)totalSamplesRead ); } - totalSamplesRead += samplesRead; + debug("loadSampleIntoBuffer: final output buffer size is " + outBuffer.getBufferSize() ); } - if ( totalSamplesRead != totalSampleCount ) - { - outBuffer.setBufferSize( (int)totalSamplesRead ); - } - - debug("loadSampleIntoBuffer: final output buffer size is " + outBuffer.getBufferSize() ); - stream.close(); } else @@ -922,8 +950,7 @@ public AudioInput getLineIn(int type, int bufferSize, float sampleRate, int bitD public AudioStream getInputStream(int type, int bufferSize, float sampleRate, int bitDepth) { AudioStream stream = mimp.getAudioInput( type, bufferSize, sampleRate, bitDepth ); - streams.add( stream ); - return stream; + return stream == null ? null : new TrackedAudioStream( this, stream ); } /** diff --git a/src/ddf/minim/Playable.java b/src/ddf/minim/Playable.java index ad9f2cd..846d9cd 100644 --- a/src/ddf/minim/Playable.java +++ b/src/ddf/minim/Playable.java @@ -84,12 +84,27 @@ public interface Playable int loopCount(); /** - * Sets the loop points used when looping. + * Sets the beginning and end of the section to loop when looping. * - * @param start the start of the loop in milliseconds - * @param stop the end of the loop in milliseconds + * @param begin the beginning of the loop in milliseconds + * @param end the end of the loop in milliseconds */ - void setLoopPoints(int start, int stop); + void setLoopPoints(int begin, int end); + + /** + * Gets the current millisecond position of the beginning looped section. + * + * @return the beginning of the looped section in milliseconds + */ + int getLoopBegin(); + + /** + * Gets the current millisecond position of the end of the looped section. + * This can be -1 if the length is unknown and setLoopPoints has never been called. + * + * @return the end of the looped section in milliseconds + */ + int getLoopEnd(); /** * Pauses playback. diff --git a/src/ddf/minim/Polyphonic.java b/src/ddf/minim/Polyphonic.java index c4b6208..9b9ff32 100644 --- a/src/ddf/minim/Polyphonic.java +++ b/src/ddf/minim/Polyphonic.java @@ -27,7 +27,7 @@ * @invisible * */ - +@Deprecated public interface Polyphonic { /** diff --git a/src/ddf/minim/TrackedAudioRecordingStream.java b/src/ddf/minim/TrackedAudioRecordingStream.java new file mode 100644 index 0000000..e473c89 --- /dev/null +++ b/src/ddf/minim/TrackedAudioRecordingStream.java @@ -0,0 +1,79 @@ +package ddf.minim; + +import ddf.minim.spi.AudioRecordingStream; + +// internal class used to wrap AudioRecordingStream objects returned by the active MinimServiceProvider +// to enable Minim to release references to AudioRecordingStreams closed explicitly by the user. +final class TrackedAudioRecordingStream extends TrackedAudioStream implements AudioRecordingStream +{ + + public TrackedAudioRecordingStream(Minim owningSystem, AudioRecordingStream streamToWrap) + { + super(owningSystem, streamToWrap); + } + + public void play() + { + stream.play(); + } + + public void pause() + { + stream.pause(); + } + + public boolean isPlaying() + { + return stream.isPlaying(); + } + + public void loop(int count) + { + stream.loop( count ); + } + + public void setLoopPoints(int start, int stop) + { + stream.setLoopPoints( start, stop ); + } + + public int getLoopCount() + { + return stream.getLoopCount(); + } + + public int getMillisecondPosition() + { + return stream.getMillisecondPosition(); + } + + public void setMillisecondPosition(int pos) + { + stream.setMillisecondPosition( pos ); + } + + public int getMillisecondLength() + { + return stream.getMillisecondLength(); + } + + public long getSampleFrameLength() + { + return stream.getSampleFrameLength(); + } + + public AudioMetaData getMetaData() + { + return stream.getMetaData(); + } + + public int getLoopBegin() + { + return stream.getLoopBegin(); + } + + public int getLoopEnd() + { + return stream.getLoopEnd(); + } +} diff --git a/src/ddf/minim/TrackedAudioStream.java b/src/ddf/minim/TrackedAudioStream.java new file mode 100644 index 0000000..9c20453 --- /dev/null +++ b/src/ddf/minim/TrackedAudioStream.java @@ -0,0 +1,53 @@ +package ddf.minim; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.Control; + +import ddf.minim.spi.AudioStream; + +// internal class used to wrap AudioStream objects returned by the active MinimServiceProvider +//to enable Minim to release references to AudioStreams closed explicitly by the user. +class TrackedAudioStream implements AudioStream +{ + private Minim system; + protected T stream; + + public TrackedAudioStream( Minim owningSystem, T streamToWrap ) + { + system = owningSystem; + stream = streamToWrap; + } + + public void open() + { + stream.open(); + system.addStream( stream ); + } + + public void close() + { + stream.close(); + system.removeStream( stream ); + } + + public Control[] getControls() + { + return stream.getControls(); + } + + public AudioFormat getFormat() + { + return stream.getFormat(); + } + + @SuppressWarnings("deprecation") + public float[] read() + { + return stream.read(); + } + + public int read(MultiChannelBuffer buffer) + { + return stream.read(buffer); + } +} diff --git a/src/ddf/minim/analysis/BeatDetect.java b/src/ddf/minim/analysis/BeatDetect.java index f3ac240..db30125 100644 --- a/src/ddf/minim/analysis/BeatDetect.java +++ b/src/ddf/minim/analysis/BeatDetect.java @@ -59,7 +59,7 @@ * In sound energy mode you use isOnset() to query the algorithm * and in frequency energy mode you use isOnset(int i), * isKick(), isSnare(), and - * isRange() to query particular frequnecy bands or ranges of + * isRange() to query particular frequency bands or ranges of * frequency bands. It should be noted that isKick(), * isSnare(), and isHat() merely call * isRange() with values determined by testing the algorithm @@ -87,25 +87,30 @@ public class BeatDetect */ public static final int SOUND_ENERGY = 1; - private int algorithm; - private int sampleRate; - private int timeSize; - private int valCnt; + private int algorithm; + private int sampleRate; + private int timeSize; + private int valCnt; private float[] valGraph; - private double sensitivity; + private int sensitivity; + // time incremented after every call to detect, to know how many milliseconds of audio we have processed so far. + // this value is used as part of the the sensitivity implementation + private long detectTimeMillis; // for circular buffer support private int insertAt; // vars for sEnergy private boolean isOnset; private float[] eBuffer; private float[] dBuffer; - private double lastTrueTime; + // a millisecond timer used to prevent reporting onsets until the sensitivity threshold has been reached + // see the sEnergy method + private long sensitivityTimer; // vars for fEnergy private boolean[] fIsOnset; private FFT spect; private float[][] feBuffer; private float[][] fdBuffer; - private double[] fTimer; + private long[] fTimer; private float[] varGraph; private int varCnt; @@ -116,18 +121,57 @@ public class BeatDetect * mode with meaningful values. * */ - public BeatDetect(int algo, int timeSize, float sampleRate) { - this.sampleRate = (int)sampleRate; - this.timeSize = timeSize; - if( algo == SOUND_ENERGY ) { + public BeatDetect() + { + sampleRate = 44100; + timeSize = 1024; initSEResources(); + initGraphs(); algorithm = SOUND_ENERGY; - } else if( algo == FREQ_ENERGY ) { + sensitivity = 10; + detectTimeMillis = 0; + } + + /** + * Create a BeatDetect object that is in SOUND_ENERGY mode for audio with the given sampleRate. + * timeSize will be set to 1024 so that switching to FREQ_ENERGY mode will work properly. + * + * @param sampleRate + * float: the sample rate of audio that will be passed to the detect method + * + * @related BeatDetect + */ + public BeatDetect(float sampleRate) + { + this.sampleRate = (int)sampleRate; + timeSize = 1024; + initSEResources(); + initGraphs(); + algorithm = SOUND_ENERGY; + sensitivity = 10; + detectTimeMillis = 0; + } + + /** + * Create a BeatDetect object that is in FREQ_ENERGY mode and expects a + * sample buffer with the requested attributes. + * + * @param timeSize + * int: the size of the buffer + * @param sampleRate + * float: the sample rate of the samples in the buffer + * + * @related BeatDetect + */ + public BeatDetect(int timeSize, float sampleRate) + { + this.sampleRate = (int) sampleRate; + this.timeSize = timeSize; initFEResources(); - algorithm = FREQ_ENERGY; - } - initGraphs(); - sensitivity = 0.3; + initGraphs(); + algorithm = FREQ_ENERGY; + sensitivity = 10; + detectTimeMillis = 0; } /** @@ -181,8 +225,9 @@ private void initSEResources() isOnset = false; eBuffer = new float[sampleRate / timeSize]; dBuffer = new float[sampleRate / timeSize]; - lastTrueTime = 0; //System.currentTimeMillis(); + sensitivityTimer = 0; insertAt = 0; + detectTimeMillis = 0; } private void initFEResources() @@ -193,13 +238,13 @@ private void initFEResources() fIsOnset = new boolean[numAvg]; feBuffer = new float[numAvg][sampleRate / timeSize]; fdBuffer = new float[numAvg][sampleRate / timeSize]; - fTimer = new double[numAvg]; - double start = 0.0; + fTimer = new long[numAvg]; for (int i = 0; i < fTimer.length; i++) { - fTimer[i] = start; + fTimer[i] = 0; } insertAt = 0; + detectTimeMillis = 0; } private void releaseSEResources() @@ -207,7 +252,8 @@ private void releaseSEResources() isOnset = false; eBuffer = null; dBuffer = null; - lastTrueTime = 0; + sensitivityTimer = 0; + detectTimeMillis = 0; } private void releaseFEResources() @@ -217,6 +263,7 @@ private void releaseFEResources() feBuffer = null; fdBuffer = null; fTimer = null; + detectTimeMillis = 0; } /** @@ -230,14 +277,10 @@ private void releaseFEResources() * * @related BeatDetect */ - public void detect(AudioBuffer buffer, double time) + public void detect(AudioBuffer buffer) { - detect( buffer.toArray(), time ); + detect( buffer.toArray() ); } - - public void detect(AudioBuffer buffer) { - detect(buffer.toArray(), (float)System.currentTimeMillis() / 1000.0); - } /** @@ -249,21 +292,18 @@ public void detect(AudioBuffer buffer) { * * @related BeatDetect */ - public void detect(float[] buffer, double time) { + public void detect(float[] buffer) + { switch (algorithm) { case SOUND_ENERGY: - sEnergy(buffer, time); + sEnergy(buffer); break; case FREQ_ENERGY: - fEnergy(buffer, time); + fEnergy(buffer); break; } } - - public void detect(float[] buffer) { - detect(buffer, (double)System.currentTimeMillis() / 1000.0); - } /** * In frequency energy mode this returns the number of frequency bands @@ -325,16 +365,16 @@ public float getDetectCenterFrequency(int i) * * @related BeatDetect */ - public void setSensitivity(double sec) + public void setSensitivity(int millis) { - if (sec < 0) + if (millis < 0) { Minim.error("BeatDetect: sensitivity cannot be less than zero. Defaulting to 10."); - sensitivity = 0.3; + sensitivity = 10; } else { - sensitivity = sec; + sensitivity = millis; } } @@ -409,15 +449,29 @@ public boolean isKick() * * @related BeatDetect */ - public boolean isSnare() + public boolean isSnare(boolean strict) { + int MINIMUM_LOW = 8; if (algorithm == SOUND_ENERGY) { return false; } - int lower = 8 >= spect.avgSize() ? spect.avgSize() : 8; + int lower; + if (spect.avgSize() < MINIMUM_LOW) + { + lower = spect.avgSize(); + } + else + { + lower = MINIMUM_LOW; + } int upper = spect.avgSize() - 1; - int thresh = (upper - lower) / 3 + 1; + // int thresh = 3; + int thresh = (upper - lower) / (strict ? 2 : 3); + // if (strict) + // { + // thresh += 1; + // } return isRange(lower, upper, thresh); } @@ -470,6 +524,10 @@ public boolean isRange(int low, int high, int threshold) { return false; } + if (high > spect.avgSize() - 1) + { + high = spect.avgSize() - 1; + } int num = 0; for (int i = low; i < high + 1; i++) { @@ -525,7 +583,7 @@ public boolean isRange(int low, int high, int threshold) // } // } - private void sEnergy(float[] samples, double time) + private void sEnergy(float[] samples) { // compute the energy level float level = 0; @@ -542,7 +600,7 @@ private void sEnergy(float[] samples, double time) float V = variance(eBuffer, E); // compute C using a linear digression of C with V float C = (-0.0025714f * V) + 1.5142857f; - // filter negaive values + // filter negative values float diff = (float)Math.max(instant - C * E, 0); pushVal(diff); // find the average of only the positive values in dBuffer @@ -552,7 +610,7 @@ private void sEnergy(float[] samples, double time) pushVar(diff2); // report false if it's been less than 'sensitivity' // milliseconds since the last true value - if (time - lastTrueTime < sensitivity) + if (detectTimeMillis - sensitivityTimer < sensitivity) { isOnset = false; } @@ -561,7 +619,7 @@ private void sEnergy(float[] samples, double time) else if (diff2 > 0 && instant > 2) { isOnset = true; - lastTrueTime = time; + sensitivityTimer = detectTimeMillis; } // OMG it wasn't true! else @@ -572,10 +630,14 @@ else if (diff2 > 0 && instant > 2) dBuffer[insertAt] = diff; insertAt++; if (insertAt == eBuffer.length) + { insertAt = 0; + } + // advance the current time by the number of milliseconds this buffer represents + detectTimeMillis += (long)(((float)samples.length / sampleRate)*1000); } - private void fEnergy(float[] in, double time) + private void fEnergy(float[] in) { spect.forward(in); float instant, E, V, C, diff, dAvg, diff2; @@ -588,14 +650,14 @@ private void fEnergy(float[] in, double time) diff = (float)Math.max(instant - C * E, 0); dAvg = specAverage(fdBuffer[i]); diff2 = (float)Math.max(diff - dAvg, 0); - if (time - fTimer[i] < sensitivity) + if (detectTimeMillis - fTimer[i] < sensitivity) { fIsOnset[i] = false; } else if (diff2 > 0) { fIsOnset[i] = true; - fTimer[i] = time; + fTimer[i] = detectTimeMillis; } else { @@ -609,6 +671,8 @@ else if (diff2 > 0) { insertAt = 0; } + // advance the current time by the number of milliseconds this buffer represents + detectTimeMillis += (long)(((float)in.length / sampleRate)*1000); } private void pushVal(float v) diff --git a/src/ddf/minim/effects/Convolver.java b/src/ddf/minim/effects/Convolver.java index c2c03eb..754961d 100644 --- a/src/ddf/minim/effects/Convolver.java +++ b/src/ddf/minim/effects/Convolver.java @@ -39,6 +39,7 @@ * @see Convolution * */ +@Deprecated public class Convolver implements AudioEffect { protected float[] kernal; diff --git a/src/ddf/minim/effects/IIRFilter.java b/src/ddf/minim/effects/IIRFilter.java index c491dbe..5b70c84 100644 --- a/src/ddf/minim/effects/IIRFilter.java +++ b/src/ddf/minim/effects/IIRFilter.java @@ -34,6 +34,7 @@ * @author Damien Di Fede * */ +@SuppressWarnings("deprecation") public abstract class IIRFilter extends UGen implements AudioEffect { public final UGenInput audio; diff --git a/src/ddf/minim/javasound/JSAudioInput.java b/src/ddf/minim/javasound/JSAudioInput.java index 027fbe4..aee4c27 100644 --- a/src/ddf/minim/javasound/JSAudioInput.java +++ b/src/ddf/minim/javasound/JSAudioInput.java @@ -29,6 +29,7 @@ import ddf.minim.spi.AudioStream; // This is our AudioInput! +@SuppressWarnings("deprecation") final class JSAudioInput extends Thread implements AudioStream { diff --git a/src/ddf/minim/javasound/JSAudioOutput.java b/src/ddf/minim/javasound/JSAudioOutput.java index c5d39e7..cdd61fb 100644 --- a/src/ddf/minim/javasound/JSAudioOutput.java +++ b/src/ddf/minim/javasound/JSAudioOutput.java @@ -30,6 +30,7 @@ import ddf.minim.spi.AudioOut; import ddf.minim.spi.AudioStream; +@SuppressWarnings("deprecation") final class JSAudioOutput extends Thread implements AudioOut { private AudioListener listener; @@ -57,7 +58,7 @@ final class JSAudioOutput extends Thread implements AudioOut finished = false; line = sdl; } - + public void run() { line.start(); diff --git a/src/ddf/minim/javasound/JSAudioRecording.java b/src/ddf/minim/javasound/JSAudioRecording.java index 17b0f83..8a6ad70 100644 --- a/src/ddf/minim/javasound/JSAudioRecording.java +++ b/src/ddf/minim/javasound/JSAudioRecording.java @@ -19,6 +19,7 @@ package ddf.minim.javasound; import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; import javax.sound.sampled.Control; import javax.sound.sampled.SourceDataLine; @@ -297,7 +298,8 @@ public synchronized void setLoopPoints(int start, int stop) { loopBegin = start; } - if ( stop <= getMillisecondLength() && stop > start ) + + if ( stop <= getMillisecondLength() && stop > loopBegin ) { loopEnd = (int)AudioUtils.millis2BytesFrameAligned( stop, format ); } @@ -357,4 +359,19 @@ public int read(MultiChannelBuffer buffer) { return 0; } + + public int getLoopBegin() + { + return loopBegin; + } + + public int getLoopEnd() + { + if ( loopEnd != AudioSystem.NOT_SPECIFIED ) + { + return (int)AudioUtils.bytes2Millis( loopEnd, format ); + } + + return AudioSystem.NOT_SPECIFIED; + } } diff --git a/src/ddf/minim/javasound/JSAudioRecordingClip.java b/src/ddf/minim/javasound/JSAudioRecordingClip.java index ec59c38..e8edc69 100644 --- a/src/ddf/minim/javasound/JSAudioRecordingClip.java +++ b/src/ddf/minim/javasound/JSAudioRecordingClip.java @@ -35,6 +35,8 @@ class JSAudioRecordingClip implements AudioRecording private int loopCount; private AudioMetaData meta; private boolean playing; + private int loopBegin; + private int loopEnd; JSAudioRecordingClip(Clip clip, AudioMetaData mdata) { @@ -101,7 +103,10 @@ public void loop(int count) public void setLoopPoints(int start, int end) { + // note: this is wrong, but this class is not used, so it doesn't matter. c.setLoopPoints( start, end ); + loopBegin = start; + loopEnd = end; } public void setMillisecondPosition(int pos) @@ -158,4 +163,14 @@ public int read(MultiChannelBuffer buffer) { return 0; } + + public int getLoopBegin() + { + return loopBegin; + } + + public int getLoopEnd() + { + return loopEnd; + } } diff --git a/src/ddf/minim/javasound/JSBaseAudioRecordingStream.java b/src/ddf/minim/javasound/JSBaseAudioRecordingStream.java index 9e3eae4..cfe4264 100644 --- a/src/ddf/minim/javasound/JSBaseAudioRecordingStream.java +++ b/src/ddf/minim/javasound/JSBaseAudioRecordingStream.java @@ -23,6 +23,7 @@ import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; import javax.sound.sampled.Control; import javax.sound.sampled.SourceDataLine; @@ -35,6 +36,7 @@ import ddf.minim.MultiChannelBuffer; import ddf.minim.spi.AudioRecordingStream; +@SuppressWarnings("deprecation") abstract class JSBaseAudioRecordingStream implements Runnable, AudioRecordingStream { @@ -118,7 +120,14 @@ abstract class JSBaseAudioRecordingStream implements Runnable, play = false; numLoops = 0; loopBegin = 0; - loopEnd = (int)AudioUtils.millis2BytesFrameAligned( msLen, format ); + if (msLen != AudioSystem.NOT_SPECIFIED) + { + loopEnd = (int)AudioUtils.millis2BytesFrameAligned( msLen, format ); + } + else + { + loopEnd = AudioSystem.NOT_SPECIFIED; + } silence = new float[bufferSize]; iothread = null; @@ -239,10 +248,36 @@ private int readBytes() return bytesRead; } + + // update the loop state and return true if we reset our position to the beginning of the loop + private boolean updateLoop() + { + if ( loop && numLoops == 0 ) + { + loop = false; + pause(); + } + else if ( loop ) + { + //System.out.println("Returning to loopBegin because else if loop"); + if ( numLoops != Minim.LOOP_CONTINUOUSLY ) + { + numLoops--; + } + setMillisecondPosition( loopBegin ); + return true; + } + + return false; + } private void readBytesLoop() { - int toLoopEnd = loopEnd - totalBytesRead; + // start with silence + Arrays.fill(rawBytes, (byte)0); + + // if the loop end isn't specified, that probably means we are dealing with an internet stream. + int toLoopEnd = loopEnd == AudioSystem.NOT_SPECIFIED ? rawBytes.length : loopEnd - totalBytesRead; if ( toLoopEnd <= 0 ) { //System.out.println("Returning to loopBegin because toLoopEnd <= 0"); @@ -257,40 +292,30 @@ private void readBytesLoop() setMillisecondPosition( loopBegin ); readBytesLoop(); } - else - { - Arrays.fill(rawBytes, (byte)0); - } return; } + + // end of loop is closer than length of bytes array, + // so read only as much as we need, then do loop logic if ( toLoopEnd < rawBytes.length ) { readBytesWrap( toLoopEnd, 0 ); - if ( loop && numLoops == 0 ) - { - loop = false; - pause(); - } - else if ( loop ) + // if we looped, fill the rest of the buffer + if ( updateLoop() ) { - //System.out.println("Returning to loopBegin because else if loop"); - if ( numLoops != Minim.LOOP_CONTINUOUSLY ) - { - numLoops--; - } - setMillisecondPosition( loopBegin ); - readBytesWrap( rawBytes.length - toLoopEnd, toLoopEnd ); + readBytesWrap( rawBytes.length - toLoopEnd, toLoopEnd ); } } else { + // loop end is either further away than the length of our buffer, or it is unknown, so we try to read a full buffer readBytesWrap( rawBytes.length, 0 ); } } // read toRead bytes from ais into rawBytes. // we assume here that if we get to the end of the file - // that we should wrap around to the beginning + // that we should wrap around to loopStart private void readBytesWrap(int toRead, int offset) { int bytesRead = 0; @@ -304,14 +329,13 @@ private void readBytesWrap(int toRead, int offset) { actualRead = ais.read( rawBytes, bytesRead + offset, toRead - bytesRead ); } + // if we reached EOF we update the loop logic and possibly continue if ( -1 == actualRead ) { - //System.out.println("!!!!!!! Looping with numLoops " + numLoops); - setMillisecondPosition( 0 ); - if ( numLoops != Minim.LOOP_CONTINUOUSLY ) - { - numLoops--; - } + if (!updateLoop()) + { + break; + } } else if ( actualRead == 0 ) { @@ -327,7 +351,6 @@ else if ( actualRead == 0 ) totalBytesRead += actualRead; } } - } catch ( IOException ioe ) { @@ -391,7 +414,7 @@ else if ( buffer.getChannelCount() == Minim.STEREO ) } } - private synchronized void process() + private synchronized void process() { synchronized ( buffer ) { @@ -497,30 +520,69 @@ public int getLoopCount() // without having to make a new AudioInputStream public void setLoopPoints(int start, int stop) { - if ( start <= 0 || start > stop ) + // first see if the end-point is in range and if not, clamp to the end of the file + int len = getMillisecondLength(); + if ( stop <= 0 ) { - loopBegin = 0; + // if stop is negative, we set the loop point to the end of the file + if ( len == AudioSystem.NOT_SPECIFIED ) + { + loopEnd = AudioSystem.NOT_SPECIFIED; + } + else + { + loopEnd = (int)AudioUtils.millis2BytesFrameAligned(len, format); + } + + stop = len; } else { - loopBegin = start; + // if stop is positive, we clamp it to the length of the file, if the length is known + if ( len != AudioSystem.NOT_SPECIFIED && stop > len ) + { + stop = len; + } + loopEnd = (int)AudioUtils.millis2BytesFrameAligned( stop, format ); } - if ( stop <= getMillisecondLength() && stop > start ) + + if ( start < 0 ) { - loopEnd = (int)AudioUtils.millis2BytesFrameAligned( stop, format ); + // if start is negative we clamp to the beginning of the file + loopBegin = 0; } else { - loopEnd = (int)AudioUtils.millis2BytesFrameAligned( - getMillisecondLength(), format ); - } + // if start is positive we clamp it to stop + if ( stop > 0 && start >= stop ) + { + start = stop - 1; + } + loopBegin = start; + } + } + + public int getLoopBegin() + { + return loopBegin; + } + + public int getLoopEnd() + { + if ( loopEnd != AudioSystem.NOT_SPECIFIED ) + { + return (int)AudioUtils.bytes2Millis( loopEnd, format ); + } + + return AudioSystem.NOT_SPECIFIED; } public int getMillisecondPosition() { int pos = (int)AudioUtils.bytes2Millis( totalBytesRead, format ); - // never report a position that is greater than the length of the stream - return Math.min( pos, getMillisecondLength() ); + // never report a position that is greater than the length of the stream, unless the length of the stream is unknown + int length = getMillisecondLength(); + return length == AudioSystem.NOT_SPECIFIED ? pos : Math.min( pos, length ); } public void setMillisecondPosition(int millis) diff --git a/src/ddf/minim/javasound/JSBufferedSampleRecorder.java b/src/ddf/minim/javasound/JSBufferedSampleRecorder.java index d12085d..1f75d5b 100644 --- a/src/ddf/minim/javasound/JSBufferedSampleRecorder.java +++ b/src/ddf/minim/javasound/JSBufferedSampleRecorder.java @@ -99,6 +99,7 @@ public String filePath() * Saves the audio in the internal buffer to a file using the current settings for * file type and file name. */ + @SuppressWarnings("resource") // for our return value public AudioRecordingStream save() { if ( isRecording() ) diff --git a/src/ddf/minim/javasound/JSMinim.java b/src/ddf/minim/javasound/JSMinim.java index 9434e85..7046890 100644 --- a/src/ddf/minim/javasound/JSMinim.java +++ b/src/ddf/minim/javasound/JSMinim.java @@ -34,7 +34,6 @@ import javax.sound.sampled.AudioSystem; import javax.sound.sampled.Clip; import javax.sound.sampled.DataLine; -import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.Mixer; import javax.sound.sampled.SourceDataLine; import javax.sound.sampled.TargetDataLine; @@ -63,6 +62,7 @@ * */ +@SuppressWarnings("deprecation") public class JSMinim implements MinimServiceProvider { private boolean debug; @@ -186,14 +186,13 @@ void debug(String s) void error(String s) { - // this is always annoying junk, not real errors - /*System.out.println("==== JavaSound Minim Error ===="); - String[] lines = s.split("\n"); - for(int i = 0; i < lines.length; i++) - { - System.out.println("==== " + lines[i]); - } - System.out.println();*/ + // System.out.println("==== JavaSound Minim Error ===="); + // String[] lines = s.split("\n"); + // for(int i = 0; i < lines.length; i++) + // { + // System.out.println("==== " + lines[i]); + // } + // System.out.println(); } public SampleRecorder getSampleRecorder(Recordable source, String fileName, @@ -261,6 +260,7 @@ else if (ext.equals(Minim.SND.getExtension())) return recorder; } + @SuppressWarnings("resource") // remove warning about ais not being closed. it is used by AudioRecordingStream, so should not be. public AudioRecordingStream getAudioRecordingStream(String filename, int bufferSize, boolean inMemory) { @@ -299,10 +299,10 @@ public AudioRecordingStream getAudioRecordingStream(String filename, if (props.containsKey("duration")) { Long dur = (Long)props.get("duration"); - if ( dur.longValue() > 0 ) - { - lengthInMillis = dur.longValue() / 1000; - } + if ( dur.longValue() > 0 ) + { + lengthInMillis = dur.longValue() / 1000; + } } MP3MetaData meta = new MP3MetaData(filename, lengthInMillis, props); mstream = new JSMPEGAudioRecordingStream(this, meta, ais, decAis, line, bufferSize); @@ -324,7 +324,6 @@ public AudioRecordingStream getAudioRecordingStream(String filename, return mstream; } - @SuppressWarnings("unchecked") private Map getID3Tags(String filename) { debug("Getting the properties."); @@ -332,25 +331,35 @@ private Map getID3Tags(String filename) try { MpegAudioFileReader reader = new MpegAudioFileReader(this); - InputStream stream = (InputStream)createInput.invoke(fileLoader, filename); - if ( stream != null ) - { - AudioFileFormat baseFileFormat = reader.getAudioFileFormat( - stream, - stream.available()); - stream.close(); - if (baseFileFormat instanceof TAudioFileFormat) + AudioFileFormat baseFileFormat = null; + // If the file is on the internet, we have to retrieve the format using the URL. + // while we expect that createInput will return a suitable stream for a URL, + // we can't count on stream.available() to return the full size of the file. + if (filename.startsWith("http")) + { + baseFileFormat = reader.getAudioFileFormat( new URL(filename) ); + } + else + { + InputStream stream = (InputStream)createInput.invoke(fileLoader, filename); + if ( stream != null ) { - TAudioFileFormat fileFormat = (TAudioFileFormat)baseFileFormat; - props = (Map)fileFormat.properties(); - if (props.size() == 0) - { - error("No file properties available for " + filename + "."); - } - else - { - debug("File properties: " + props.toString()); - } + baseFileFormat = reader.getAudioFileFormat(stream, stream.available()); + stream.close(); + } + } + + if (baseFileFormat instanceof TAudioFileFormat) + { + TAudioFileFormat fileFormat = (TAudioFileFormat)baseFileFormat; + props = (Map)fileFormat.properties(); + if (props.size() == 0) + { + error("No file properties available for " + filename + "."); + } + else + { + debug("File properties: " + props.toString()); } } } @@ -371,6 +380,7 @@ private Map getID3Tags(String filename) return props; } + @SuppressWarnings("resource") // suppress warning about potential leak of line (will be closed by JSAudioInput) public AudioStream getAudioInput(int type, int bufferSize, float sampleRate, int bitDepth) { @@ -378,7 +388,7 @@ public AudioStream getAudioInput(int type, int bufferSize, { throw new IllegalArgumentException("Unsupported bit depth, use either 8 or 16."); } - AudioFormat format = new AudioFormat(sampleRate, bitDepth, type, true, false); + AudioFormat format = new AudioFormat(sampleRate, bitDepth, type, true, false); TargetDataLine line = getTargetDataLine(format, bufferSize * 4); if (line != null) { @@ -387,6 +397,7 @@ public AudioStream getAudioInput(int type, int bufferSize, return null; } + @SuppressWarnings("resource") public AudioSample getAudioSample(String filename, int bufferSize) { AudioInputStream ais = getAudioInputStream(filename); @@ -412,10 +423,13 @@ public AudioSample getAudioSample(String filename, int bufferSize) // be much shorter than the decoded version. so we use the // duration of the file to figure out how many bytes the // decoded file will be. - long dur = ((Long)props.get("duration")).longValue(); - int toRead = (int)AudioUtils.millis2Bytes(dur / 1000, format); - samples = loadFloatAudio(ais, toRead); - meta = new MP3MetaData(filename, dur / 1000, props); + if (props.containsKey( "duration" )) + { + long dur = ((Long)props.get("duration")).longValue(); + int toRead = (int)AudioUtils.millis2Bytes(dur / 1000, format); + samples = loadFloatAudio(ais, toRead); + meta = new MP3MetaData(filename, dur / 1000, props); + } } else { @@ -423,19 +437,37 @@ public AudioSample getAudioSample(String filename, int bufferSize) long length = AudioUtils.frames2Millis(samples.getSampleCount(), format); meta = new BasicMetaData(filename, length, samples.getSampleCount()); } - AudioOut out = getAudioOutput(format.getChannels(), - bufferSize, - format.getSampleRate(), - format.getSampleSizeInBits()); - if (out != null) + + // close the ais because we are done with it + try + { + ais.close(); + } + catch ( IOException e ) { - SampleSignal ssig = new SampleSignal(samples); - out.setAudioSignal(ssig); - return new JSAudioSample(meta, ssig, out); + e.printStackTrace(); + } + + if ( samples != null ) + { + AudioOut out = getAudioOutput(format.getChannels(), + bufferSize, + format.getSampleRate(), + format.getSampleSizeInBits()); + if (out != null) + { + SampleSignal ssig = new SampleSignal(samples); + out.setAudioSignal(ssig); + return new JSAudioSample(meta, ssig, out); + } + else + { + error("Couldn't acquire an output."); + } } else { - error("Couldn't acquire an output."); + error("Couldn't load " + filename + " because the length is unknown."); } } return null; @@ -479,8 +511,8 @@ private JSAudioSample getAudioSampleImp(FloatSampleBuffer samples, AudioFormat f return null; } - public AudioOut getAudioOutput(int type, int bufferSize, - float sampleRate, int bitDepth) + @SuppressWarnings("resource") // suppress warning about unclosed line + public AudioOut getAudioOutput(int type, int bufferSize, float sampleRate, int bitDepth) { if (bitDepth != 8 && bitDepth != 16) { @@ -496,6 +528,7 @@ public AudioOut getAudioOutput(int type, int bufferSize, } /** @deprecated */ + @SuppressWarnings("resource") // this method is never called public AudioRecording getAudioRecordingClip(String filename) { Clip clip = null; @@ -542,7 +575,7 @@ public AudioRecording getAudioRecordingClip(String filename) { error("File format not supported."); return null; - } + } } if (meta == null) { @@ -553,11 +586,12 @@ public AudioRecording getAudioRecordingClip(String filename) } /** @deprecated */ + @SuppressWarnings("resource") // ais is closed, line need not be public AudioRecording getAudioRecording(String filename) { AudioMetaData meta = null; AudioInputStream ais = getAudioInputStream(filename); - byte[] samples; + byte[] samples = null; if (ais != null) { AudioFormat format = ais.getFormat(); @@ -578,10 +612,17 @@ public AudioRecording getAudioRecording(String filename) // be much shorter than the decoded version. so we use the // duration of the file to figure out how many bytes the // decoded file will be. - long dur = ((Long)props.get("duration")).longValue(); - int toRead = (int)AudioUtils.millis2Bytes(dur / 1000, format); - samples = loadByteAudio(ais, toRead); - meta = new MP3MetaData(filename, dur / 1000, props); + if (props.containsKey( "duration" )) + { + long dur = ((Long)props.get("duration")).longValue(); + int toRead = (int)AudioUtils.millis2Bytes(dur / 1000, format); + samples = loadByteAudio(ais, toRead); + meta = new MP3MetaData(filename, dur / 1000, props); + } + else + { + Minim.error( "Can't load " + filename + " because the length of the file is unknown." ); + } } else { @@ -589,10 +630,22 @@ public AudioRecording getAudioRecording(String filename) long length = AudioUtils.bytes2Millis(samples.length, format); meta = new BasicMetaData(filename, length, samples.length); } - SourceDataLine line = getSourceDataLine(format, 2048); - if ( line != null ) + + try + { + ais.close(); + } + catch ( IOException e ) + { + } + + if (samples != null) { - return new JSAudioRecording(this, samples, line, meta); + SourceDataLine line = getSourceDataLine(format, 2048); + if ( line != null ) + { + return new JSAudioRecording(this, samples, line, meta); + } } } return null; @@ -661,7 +714,6 @@ private byte[] loadByteAudio(AudioInputStream ais, int toRead) AudioInputStream getAudioInputStream(String filename) { AudioInputStream ais = null; - BufferedInputStream bis = null; if (filename.startsWith("http")) { try @@ -689,7 +741,8 @@ AudioInputStream getAudioInputStream(String filename) if ( is != null ) { debug("Base input stream is: " + is.toString()); - bis = new BufferedInputStream(is); + @SuppressWarnings("resource") + BufferedInputStream bis = new BufferedInputStream(is); ais = getAudioInputStream(bis); if ( ais != null ) @@ -800,6 +853,7 @@ AudioInputStream getAudioInputStream(AudioFormat targetFormat, } } + @SuppressWarnings("resource") // suppress warning about unclosed line SourceDataLine getSourceDataLine(AudioFormat format, int bufferSize) { SourceDataLine line = null; @@ -844,6 +898,7 @@ SourceDataLine getSourceDataLine(AudioFormat format, int bufferSize) return line; } + @SuppressWarnings("resource") // suppress warning about unclosed line TargetDataLine getTargetDataLine(AudioFormat format, int bufferSize) { TargetDataLine line = null; diff --git a/src/ddf/minim/javasound/JSStreamingSampleRecorder.java b/src/ddf/minim/javasound/JSStreamingSampleRecorder.java index cbbe14a..a5c9693 100644 --- a/src/ddf/minim/javasound/JSStreamingSampleRecorder.java +++ b/src/ddf/minim/javasound/JSStreamingSampleRecorder.java @@ -124,6 +124,7 @@ public boolean isRecording() /** * Finishes the recording process by closing the file. */ + @SuppressWarnings("resource") // for the return value public AudioRecordingStream save() { try diff --git a/src/ddf/minim/javasound/MpegAudioFileReader.java b/src/ddf/minim/javasound/MpegAudioFileReader.java index 8901ece..a5f3c56 100644 --- a/src/ddf/minim/javasound/MpegAudioFileReader.java +++ b/src/ddf/minim/javasound/MpegAudioFileReader.java @@ -335,8 +335,8 @@ else if (((head[0] == 'O') | (head[0] == 'o')) try { Bitstream m_bitstream = new Bitstream(pis); - aff_properties.put("mp3.header.pos", - new Integer(m_bitstream.header_pos())); + int streamPos = m_bitstream.header_pos(); + aff_properties.put("mp3.header.pos", new Integer(streamPos)); Header m_header = m_bitstream.readFrame(); if ( m_header == null ) { @@ -376,10 +376,13 @@ else if (((head[0] == 'O') | (head[0] == 'o')) { throw new UnsupportedAudioFileException("Invalid FrameRate : " + FrameRate); } + // remove header size from the length used to estimate total frames and duration + int tmpLength = mLength; + if ((streamPos > 0) && (mLength != AudioSystem.NOT_SPECIFIED) && (streamPos < mLength)) tmpLength = tmpLength - streamPos; if (mLength != AudioSystem.NOT_SPECIFIED) { aff_properties.put("mp3.length.bytes", new Integer(mLength)); - nTotalFrames = m_header.max_number_of_frames(mLength); + nTotalFrames = m_header.max_number_of_frames(tmpLength); aff_properties.put("mp3.length.frames", new Integer(nTotalFrames)); } BitRate = m_header.bitrate(); @@ -390,7 +393,7 @@ else if (((head[0] == 'O') | (head[0] == 'o')) aff_properties.put("mp3.version.encoding", encoding.toString()); if (mLength != AudioSystem.NOT_SPECIFIED) { - nTotalMS = Math.round(m_header.total_ms(mLength)); + nTotalMS = Math.round(m_header.total_ms(tmpLength)); aff_properties.put("duration", new Long((long)nTotalMS * 1000L)); } aff_properties.put("mp3.copyright", new Boolean(m_header.copyright())); @@ -401,7 +404,8 @@ else if (((head[0] == 'O') | (head[0] == 'o')) if (id3v2 != null) { aff_properties.put("mp3.id3tag.v2", id3v2); - //parseID3v2Frames(id3v2, aff_properties); // this just spams junk + parseID3v2Frames(id3v2, aff_properties); + id3v2.close(); } if (TDebug.TraceAudioFileReader) TDebug.out(m_header.toString()); @@ -437,7 +441,7 @@ else if (((head[0] == 'O') | (head[0] == 'o')) inputStream.read(id3v1, 0, id3v1.length); if ((id3v1[0] == 'T') && (id3v1[1] == 'A') && (id3v1[2] == 'G')) { - //parseID3v1Frames(id3v1, aff_properties); + parseID3v1Frames(id3v1, aff_properties); } } AudioFormat format = new MpegAudioFormat(encoding, (float)nFrequency, @@ -465,6 +469,7 @@ else if (((head[0] == 'O') | (head[0] == 'o')) /** * Returns AudioInputStream from file. */ + @SuppressWarnings("resource") public AudioInputStream getAudioInputStream(File file) throws UnsupportedAudioFileException, IOException { @@ -517,6 +522,7 @@ public AudioInputStream getAudioInputStream(URL url) if (isShout == true) { // Yes + @SuppressWarnings("resource") IcyInputStream icyStream = new IcyInputStream(bInputStream); icyStream.addTagParseListener(IcyListener.getInstance()); inputStream = icyStream; @@ -528,6 +534,7 @@ public AudioInputStream getAudioInputStream(URL url) if (metaint != null) { // Yes, it might be icecast 2 mp3 stream. + @SuppressWarnings("resource") IcyInputStream icyStream = new IcyInputStream(bInputStream, metaint); icyStream.addTagParseListener(IcyListener.getInstance()); inputStream = icyStream; @@ -891,5 +898,6 @@ protected void loadShoutcastInfo(InputStream input, HashMap prop props.put("mp3.shoutcast.metadata." + key, value); } } + icy.close(); } } diff --git a/src/ddf/minim/javasound/MpegAudioFileReaderWorkaround.java b/src/ddf/minim/javasound/MpegAudioFileReaderWorkaround.java index 2e4391e..8e471b9 100644 --- a/src/ddf/minim/javasound/MpegAudioFileReaderWorkaround.java +++ b/src/ddf/minim/javasound/MpegAudioFileReaderWorkaround.java @@ -80,6 +80,7 @@ public AudioInputStream getAudioInputStream(URL url, String userAgent) { // Yes system.debug("URL is a shoutcast server."); + @SuppressWarnings("resource") IcyInputStream icyStream = new IcyInputStream(bInputStream); icyStream.addTagParseListener(IcyListener.getInstance()); inputStream = icyStream; @@ -92,6 +93,7 @@ public AudioInputStream getAudioInputStream(URL url, String userAgent) { // Yes, it might be icecast 2 mp3 stream. system.debug("URL is probably an icecast 2 mp3 stream"); + @SuppressWarnings("resource") IcyInputStream icyStream = new IcyInputStream(bInputStream, metaint); icyStream.addTagParseListener(IcyListener.getInstance()); inputStream = icyStream; diff --git a/src/ddf/minim/javasound/SampleSignal.java b/src/ddf/minim/javasound/SampleSignal.java index 8ce0033..a19d224 100644 --- a/src/ddf/minim/javasound/SampleSignal.java +++ b/src/ddf/minim/javasound/SampleSignal.java @@ -22,6 +22,7 @@ import ddf.minim.AudioSignal; import ddf.minim.Minim; +@SuppressWarnings("deprecation") class SampleSignal implements AudioSignal { private FloatSampleBuffer buffer; diff --git a/src/ddf/minim/signals/Oscillator.java b/src/ddf/minim/signals/Oscillator.java index 0ffcb05..2b9cf8b 100644 --- a/src/ddf/minim/signals/Oscillator.java +++ b/src/ddf/minim/signals/Oscillator.java @@ -35,6 +35,7 @@ * @author Damien Di Fede * */ +@Deprecated public abstract class Oscillator implements AudioSignal { /** The float value of 2*PI. Provided as a convenience for subclasses. */ diff --git a/src/ddf/minim/signals/PinkNoise.java b/src/ddf/minim/signals/PinkNoise.java index b77687c..0a38c24 100644 --- a/src/ddf/minim/signals/PinkNoise.java +++ b/src/ddf/minim/signals/PinkNoise.java @@ -27,6 +27,7 @@ * @see Pink Noise * */ +@SuppressWarnings("deprecation") public class PinkNoise implements AudioSignal { protected float amp; diff --git a/src/ddf/minim/signals/PulseWave.java b/src/ddf/minim/signals/PulseWave.java index df8f19a..4c9b994 100644 --- a/src/ddf/minim/signals/PulseWave.java +++ b/src/ddf/minim/signals/PulseWave.java @@ -26,6 +26,7 @@ * @author Damien Di Fede * @see Pulse Wave */ +@Deprecated public class PulseWave extends Oscillator { private float width; diff --git a/src/ddf/minim/signals/SawWave.java b/src/ddf/minim/signals/SawWave.java index 1ffbe79..d59cc61 100644 --- a/src/ddf/minim/signals/SawWave.java +++ b/src/ddf/minim/signals/SawWave.java @@ -24,6 +24,7 @@ * @see Saw Wave * */ +@Deprecated public class SawWave extends Oscillator { diff --git a/src/ddf/minim/signals/SineWave.java b/src/ddf/minim/signals/SineWave.java index 9337fe5..a1dd897 100644 --- a/src/ddf/minim/signals/SineWave.java +++ b/src/ddf/minim/signals/SineWave.java @@ -25,6 +25,7 @@ * @see Sine Wave * */ +@Deprecated public class SineWave extends Oscillator { diff --git a/src/ddf/minim/signals/SquareWave.java b/src/ddf/minim/signals/SquareWave.java index 3492d1a..7114824 100644 --- a/src/ddf/minim/signals/SquareWave.java +++ b/src/ddf/minim/signals/SquareWave.java @@ -24,6 +24,7 @@ * @author ddf * @see Square Wave */ +@Deprecated public class SquareWave extends Oscillator { diff --git a/src/ddf/minim/signals/TriangleWave.java b/src/ddf/minim/signals/TriangleWave.java index 86bffc2..a4c4618 100644 --- a/src/ddf/minim/signals/TriangleWave.java +++ b/src/ddf/minim/signals/TriangleWave.java @@ -24,7 +24,7 @@ * @author Damien Di Fede * @see Triangle Wave */ - +@Deprecated public class TriangleWave extends Oscillator { /** diff --git a/src/ddf/minim/signals/WhiteNoise.java b/src/ddf/minim/signals/WhiteNoise.java index 58821f9..4095421 100644 --- a/src/ddf/minim/signals/WhiteNoise.java +++ b/src/ddf/minim/signals/WhiteNoise.java @@ -26,6 +26,7 @@ * @author Damien Di Fede * @see White Noise */ +@Deprecated public class WhiteNoise implements AudioSignal { protected float amp; diff --git a/src/ddf/minim/spi/AudioOut.java b/src/ddf/minim/spi/AudioOut.java index ef6f163..4184f8d 100644 --- a/src/ddf/minim/spi/AudioOut.java +++ b/src/ddf/minim/spi/AudioOut.java @@ -30,6 +30,7 @@ * @author Damien Di Fede * */ +@SuppressWarnings("deprecation") public interface AudioOut extends AudioResource { /** diff --git a/src/ddf/minim/spi/AudioRecording.java b/src/ddf/minim/spi/AudioRecording.java index 2964371..b6cfd66 100644 --- a/src/ddf/minim/spi/AudioRecording.java +++ b/src/ddf/minim/spi/AudioRecording.java @@ -31,14 +31,14 @@ public interface AudioRecording extends AudioResource, AudioStream { /** - * Allows playback/reads of the source. + * Allows playback/reads of the source. * */ void play(); /** - * Disallows playback/reads of the source. If this is pause, all calls to read - * will generate arrays full of zeros (silence). + * Disallows playback/reads of the source. If this is pause, all calls to + * read will generate arrays full of zeros (silence). * */ void pause(); @@ -51,34 +51,51 @@ public interface AudioRecording extends AudioResource, AudioStream * times, and finally continue playback to the end of the clip. * * If the current position when this method is invoked is greater than the - * loop end point, playback simply continues to the end of the source without - * looping. + * loop end point, playback simply continues to the end of the source + * without looping. * * A count value of 0 indicates that any current looping should cease and - * playback should continue to the end of the clip. The behavior is undefined - * when this method is invoked with any other value during a loop operation. + * playback should continue to the end of the clip. The behavior is + * undefined when this method is invoked with any other value during a loop + * operation. * - * If playback is stopped during looping, the current loop status is cleared; - * the behavior of subsequent loop and start requests is not affected by an - * interrupted loop operation. + * If playback is stopped during looping, the current loop status is + * cleared; the behavior of subsequent loop and start requests is not + * affected by an interrupted loop operation. * * @param count - * the number of times playback should loop back from the loop's - * end position to the loop's start position, or - * Minim.LOOP_CONTINUOUSLY to indicate that looping should continue - * until interrupted + * the number of times playback should loop back from the loop's + * end position to the loop's start position, or + * Minim.LOOP_CONTINUOUSLY to indicate that looping should + * continue until interrupted */ void loop(int count); /** - * Sets the loops points in the source, in milliseconds + * Sets the beginning and end of the section to loop when looping. * - * @param start - * the position of the beginning of the loop - * @param stop - * the position of the end of the loop + * @param begin + * the beginning of the loop in milliseconds + * @param end + * the end of the loop in milliseconds */ - void setLoopPoints(int start, int stop); + void setLoopPoints(int begin, int end); + + /** + * Gets the current millisecond position of the beginning looped section. + * + * @return the beginning of the looped section in milliseconds + */ + int getLoopBegin(); + + /** + * Gets the current millisecond position of the end of the looped section. + * This can be -1 if the length is unknown and setLoopPoints + * has never been called. + * + * @return the end of the looped section in milliseconds + */ + int getLoopEnd(); /** * How many loops are left to go. 0 means this isn't looping and -1 means @@ -99,7 +116,7 @@ public interface AudioRecording extends AudioResource, AudioStream * Sets the current millisecond position of the source. * * @param pos - * the posititon to cue the playback head to + * the posititon to cue the playback head to */ void setMillisecondPosition(int pos); diff --git a/src/ddf/minim/spi/AudioRecordingStream.java b/src/ddf/minim/spi/AudioRecordingStream.java index a2e7a7c..20fd5ee 100644 --- a/src/ddf/minim/spi/AudioRecordingStream.java +++ b/src/ddf/minim/spi/AudioRecordingStream.java @@ -60,14 +60,30 @@ public interface AudioRecordingStream extends AudioStream void loop(int count); /** - * Sets the loops points in the source, in milliseconds + * Sets the beginning and end of the section to loop used when looping. * - * @param start - * the position of the beginning of the loop - * @param stop - * the position of the end of the loop + * @param begin + * the beginning of the loop in milliseconds + * @param end + * the end of the loop in milliseconds */ - void setLoopPoints(int start, int stop); + void setLoopPoints(int begin, int end); + + /** + * Gets the current millisecond position of the beginning of the looped section. + * + * @return the beginning of the looped section in milliseconds + */ + int getLoopBegin(); + + /** + * Gets the current millisecond position of the end of the looped section. + * This can be -1 if the length is unknown and setLoopPoints + * has never been called. + * + * @return the end of the looped section in milliseconds + */ + int getLoopEnd(); /** * How many loops are left to go. 0 means this isn't looping and -1 means diff --git a/src/ddf/minim/ugens/ADSR.java b/src/ddf/minim/ugens/ADSR.java index 6697913..e0e958e 100644 --- a/src/ddf/minim/ugens/ADSR.java +++ b/src/ddf/minim/ugens/ADSR.java @@ -258,6 +258,7 @@ public void noteOn() // ddf: reset these so that the envelope can be retriggered timeFromOff = -1.f; isTurnedOff = false; + amplitude = beforeAmplitude; } /** * Specifies that the ADSR envelope should start the release time. diff --git a/src/ddf/minim/ugens/FilePlayer.java b/src/ddf/minim/ugens/FilePlayer.java index aff30e0..b1a0539 100644 --- a/src/ddf/minim/ugens/FilePlayer.java +++ b/src/ddf/minim/ugens/FilePlayer.java @@ -33,6 +33,7 @@ public class FilePlayer extends UGen implements Playable private MultiChannelBuffer buffer; // where in the buffer we should read the next sample from private int bufferOutIndex; + private int bufferChannelCount; /** * Construct a FilePlayer that will read from iFileStream. @@ -45,7 +46,8 @@ public class FilePlayer extends UGen implements Playable public FilePlayer( AudioRecordingStream iFileStream ) { mFileStream = iFileStream; - buffer = new MultiChannelBuffer(1024, mFileStream.getFormat().getChannels()); + bufferChannelCount = mFileStream.getFormat().getChannels(); + buffer = new MultiChannelBuffer(1024, bufferChannelCount); bufferOutIndex = 0; // we'll need to do this eventually, I think. @@ -159,7 +161,13 @@ public void loop() */ public void loop(int loopCount) { - if ( isPaused ) + // AudioRecordingStream's loop method is supposed to always start looping from the loop point, + // since this is different from the stated Playable behavior, we have to stash the current position + // before calling loop so that it will start playing from the correct position. + // + // If this has never been paused before and the stream isn't playing, + // then people probably will expect the file to start playing from the loopStart, not from the beginning. + if ( isPaused || mFileStream.isPlaying() ) { int pos = mFileStream.getMillisecondPosition(); mFileStream.loop( loopCount ); @@ -167,7 +175,7 @@ public void loop(int loopCount) } else { - mFileStream.loop(loopCount); + mFileStream.loop( loopCount ); } isPaused = false; @@ -220,8 +228,8 @@ public int position() * the beginning. This will not change the play state. If an error * occurs while trying to cue, the position will not change. * If you try to cue to a negative position or try to a position - * that is greater than length(), the amount will be clamped - * to zero or length(). + * that is greater than a non-negative length(), + * the amount will be clamped to zero or length(). * * @shortdesc Sets the position to millis milliseconds from * the beginning. @@ -233,13 +241,19 @@ public int position() public void cue(int millis) { if (millis < 0) - { + { millis = 0; - } - else if (millis > length()) - { - millis = length(); - } + } + else + { + // only clamp millis to the length of the file if the length is known. + // otherwise we will try to skip what is asked and count on the underlying stream to handle it. + int len = mFileStream.getMillisecondLength(); + if (len >= 0 && millis > len) + { + millis = len; + } + } mFileStream.setMillisecondPosition(millis); // change the position in the stream invalidates our buffer, so we read a new buffer fillBuffer(); @@ -247,10 +261,10 @@ else if (millis > length()) /** * Skips millis from the current position. millis - * can be negative, which will make this skip backwards. If the skip amount - * would result in a negative position or a position that is greater than - * length(), the new position will be clamped to zero or - * length(). + * can be negative, which will make this skip backwards. + * If the skip amount would result in a negative position + * or a position that is greater than a non-negative length(), + * the new position will be clamped to zero or length(). * * @shortdesc Skips millis from the current position. * @@ -266,11 +280,8 @@ public void skip(int millis) { pos = 0; } - else if (pos > length()) - { - pos = length(); - } - //Minim.debug("AudioPlayer.skip: skipping " + millis + " milliseconds, new position is " + pos); + + Minim.debug("FilePlayer.skip: attempting to skip " + millis + " milliseconds, to position " + pos); cue( pos ); } @@ -318,22 +329,65 @@ public AudioMetaData getMetaData() return mFileStream.getMetaData(); } - /** - * Sets the loop points used when looping. - * - * @param start - * int: the start of the loop in milliseconds - * @param stop - * int: the end of the loop in milliseconds - * - * @related loop ( ) - * @related FilePlayer - */ + /** + * Sets the beginning and end of the section to loop when looping. + * These should be between 0 and the length of the file. + * If end is larger than the length of the file, + * the end of the loop will be set to the end of the file. + * If the length of the file is unknown and is positive, + * it will be used directly. + * If end is negative, the end of the loop + * will be set to the end of the file. + * If begin is greater than end + * (unless end is negative), it will be clamped + * to one millisecond before end. + * + * @param begin + * int: the beginning of the loop in milliseconds + * @param end + * int: the end of the loop in milliseconds, or -1 to set it to the end of the file + * + * @related loop ( ) + * @related getLoopBegin ( ) + * @related getLoopEnd ( ) + * @related FilePlayer + */ public void setLoopPoints(int start, int stop) { mFileStream.setLoopPoints(start, stop); } + /** + * Gets the current millisecond position of the beginning of the looped section. + * + * @return + * int: the beginning of the looped section in milliseconds + * + * @related setLoopPoints ( ) + * @related loop ( ) + * @related FilePlayer + */ + public int getLoopBegin() + { + return mFileStream.getLoopBegin(); + } + + /** + * Gets the current millisecond position of the end of the looped section. + * This can be -1 if the length is unknown and setLoopPoints has never been called. + * + * @return + * int: the end of the looped section in milliseconds + * + * @related setLoopPoints ( ) + * @related loop ( ) + * @related FilePlayer + */ + public int getLoopEnd() + { + return mFileStream.getLoopEnd(); + } + /** * Calling close will close the AudioRecordingStream that this wraps, * which is proper cleanup for using the stream. @@ -357,20 +411,20 @@ protected void uGenerate(float[] channels) if ( mFileStream.isPlaying() ) { // special case: mono expands out to all channels. - if ( buffer.getChannelCount() == 1 ) + if ( bufferChannelCount == 1 ) { Arrays.fill( channels, buffer.getSample( 0, bufferOutIndex ) ); } // we have more than one channel, don't try to fill larger channel requests - if ( buffer.getChannelCount() <= channels.length ) + else if ( bufferChannelCount <= channels.length ) { - for(int i = 0 ; i < channels.length; ++i) + for(int i = 0 ; i < bufferChannelCount; ++i) { channels[i] = buffer.getSample( i, bufferOutIndex ); } } // special case: we are stereo, output is mono. - else if ( channels.length == 1 && buffer.getChannelCount() == 2 ) + else if ( channels.length == 1 && bufferChannelCount == 2 ) { channels[0] = (buffer.getSample( 0, bufferOutIndex ) + buffer.getSample( 1, bufferOutIndex ))/2.0f; } @@ -386,5 +440,4 @@ else if ( channels.length == 1 && buffer.getChannelCount() == 2 ) Arrays.fill( channels, 0 ); } } - } diff --git a/src/ddf/minim/ugens/Frequency.java b/src/ddf/minim/ugens/Frequency.java index 21e74fe..7cced7d 100644 --- a/src/ddf/minim/ugens/Frequency.java +++ b/src/ddf/minim/ugens/Frequency.java @@ -248,7 +248,7 @@ public static Frequency ofPitch(String pitchName) if ( matcher.find() ) { String noteNameString = pitchName.substring(matcher.start(), matcher.end() ); - float noteOffset = (float) noteNameOffsets.get( noteNameString ); + float noteOffset = noteNameOffsets.get( noteNameString ).floatValue(); midiNote += noteOffset; } Minim.debug("midiNote based on noteName = " + midiNote ); diff --git a/src/ddf/minim/ugens/LiveInput.java b/src/ddf/minim/ugens/LiveInput.java index a046765..0e1d790 100644 --- a/src/ddf/minim/ugens/LiveInput.java +++ b/src/ddf/minim/ugens/LiveInput.java @@ -41,6 +41,7 @@ public void close() mInputStream.close(); } + @SuppressWarnings("deprecation") @Override protected void uGenerate(float[] channels) { diff --git a/src/ddf/minim/ugens/Sampler.java b/src/ddf/minim/ugens/Sampler.java index 2e69f45..23fd0b1 100644 --- a/src/ddf/minim/ugens/Sampler.java +++ b/src/ddf/minim/ugens/Sampler.java @@ -232,8 +232,6 @@ private class Trigger float attackAmp; // how much to increase the attack amp each sample frame float attackAmpStep; - // release time, in samples - int release; // whether we are done playing our bit of the sample or not boolean done; // whether we should start triggering in the next call to generate @@ -267,7 +265,6 @@ void generate( float[] sampleFrame ) attackLength = (int)Math.max( sampleRate() * attack.getLastValue(), 1.f ); attackAmp = 0; attackAmpStep = 1.0f / attackLength; - release = 0; sample = beginSample; outSampleCount = 0; done = false; diff --git a/src/ddf/minim/ugens/Summer.java b/src/ddf/minim/ugens/Summer.java index c35eeb7..e394104 100644 --- a/src/ddf/minim/ugens/Summer.java +++ b/src/ddf/minim/ugens/Summer.java @@ -17,6 +17,7 @@ * @author Damien Di Fede * */ +@SuppressWarnings("deprecation") public class Summer extends UGen implements AudioSignal { private ArrayList m_ugens; diff --git a/test/UnitTests.java b/test/UnitTests.java new file mode 100644 index 0000000..db6f577 --- /dev/null +++ b/test/UnitTests.java @@ -0,0 +1,160 @@ +import org.junit.jupiter.api.Test; + +import autostepper.genetic.AlgorithmParameter; +import autostepper.genetic.GeneticOptimizer; +import autostepper.misc.Averages; +import autostepper.misc.Utils; +import gnu.trove.list.array.TFloatArrayList; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; + +class UnitTests { + + TFloatArrayList inputArray = new TFloatArrayList(); + float average; + float median; + + @BeforeEach + void setUp() + { + inputArray.add(2.61f); + inputArray.add(7.08f); + inputArray.add(3.08f); + inputArray.add(3.37f); + inputArray.add(5.33f); + inputArray.add(6.65f); + inputArray.add(7.29f); + inputArray.add(9.67f); + inputArray.add(1.21f); + inputArray.add(8.45f); + inputArray.add(4.89f); + inputArray.add(5.26f); + inputArray.add(8.12f); + inputArray.add(1.24f); + inputArray.add(5.65f); + inputArray.add(0.34f); + inputArray.add(8.03f); + inputArray.add(8.89f); + inputArray.add(6.05f); + inputArray.add(7.51f); + inputArray.add(2.42f); + inputArray.add(6.78f); + inputArray.add(7.84f); + inputArray.add(1.4f); + inputArray.add(3.98f); + inputArray.add(0.77f); + inputArray.add(6.01f); + inputArray.add(7.87f); + inputArray.add(4.9f); + inputArray.add(4.53f); + inputArray.add(1.36f); + inputArray.add(8.08f); + inputArray.add(5.4f); + inputArray.add(9.92f); + inputArray.add(5.44f); + inputArray.add(4.04f); + inputArray.add(3.22f); + inputArray.add(7.82f); + inputArray.add(3.92f); + inputArray.add(5.6f); + inputArray.add(4.1f); + inputArray.add(5.15f); + inputArray.add(7.45f); + inputArray.add(8.55f); + inputArray.add(0.1f); + inputArray.add(7.98f); + inputArray.add(1.58f); + inputArray.add(7.9f); + inputArray.add(8.3f); + inputArray.add(0.92f); + inputArray.add(2.08f); + inputArray.add(0.8f); + inputArray.add(8.54f); + inputArray.add(3.27f); + inputArray.add(7.19f); + inputArray.add(8.65f); + inputArray.add(9.89f); + inputArray.add(6.36f); + inputArray.add(2.43f); + inputArray.add(9.3f); + inputArray.add(9.53f); + inputArray.add(1.03f); + inputArray.add(3.21f); + inputArray.add(9.13f); + inputArray.add(2.27f); + inputArray.add(2.17f); + inputArray.add(6.96f); + inputArray.add(3.86f); + inputArray.add(0.47f); + inputArray.add(1.46f); + inputArray.add(3.55f); + inputArray.add(5.44f); + inputArray.add(1.03f); + inputArray.add(3.12f); + inputArray.add(9.45f); + inputArray.add(5.71f); + inputArray.add(6.2f); + inputArray.add(9.59f); + inputArray.add(1.08f); + inputArray.add(6.0f); + inputArray.add(4.25f); + inputArray.add(6.71f); + inputArray.add(1.28f); + inputArray.add(3.04f); + inputArray.add(3.47f); + inputArray.add(8.68f); + inputArray.add(5.03f); + inputArray.add(7.39f); + inputArray.add(0.13f); + inputArray.add(1.13f); + inputArray.add(0.36f); + inputArray.add(6.12f); + inputArray.add(6.2f); + inputArray.add(6.51f); + inputArray.add(4.24f); + inputArray.add(0.25f); + inputArray.add(0.09f); + inputArray.add(6.07f); + inputArray.add(4.34f); + inputArray.add(0.1f); + + TFloatArrayList copyForMedian = new TFloatArrayList(inputArray); + copyForMedian.sort(); + median = copyForMedian.get(copyForMedian.size() / 2); + average = inputArray.sum() / inputArray.size(); + System.out.println("Median: " + median + " Average: " + average); + } + + @Test + void checkOgAlgorithm() { + float threshold = 0.5f; + boolean closestToInteger = false; + + + float value = Averages.getMostCommonPhr00t(inputArray, threshold, closestToInteger); + assertEquals(7.511291f, value); + } + + @Test + void checkNewAlgorithm() { + float threshold = 0.5f; + boolean closestToInteger = false; + + + float value = Averages.getMostCommonFightlapa(inputArray, threshold, closestToInteger); + assertEquals(5.26f, value); + } + + + @Test + void checkGenesGeneratorIntRange() + { + for (int i = 0; i < 200; i++) + { + float val = GeneticOptimizer.mutateSingleGene(AlgorithmParameter.KICK_LOW_FREQ.value()); + assertTrue(val >= 1 && val <= 8); + } + } +}