@@ -0,0 +1,11 @@ | |||
/.gradle | |||
/.idea | |||
/out | |||
build/ | |||
*.iml | |||
*.ipr | |||
*.iws | |||
.classpath | |||
.project | |||
.settings/ | |||
bin/ |
@@ -0,0 +1,16 @@ | |||
apply plugin: 'kotlin' | |||
group 'me.jfenn' | |||
version '0.0.1' | |||
sourceSets { | |||
main.kotlin.srcDirs = main.java.srcDirs = ['src'] | |||
test.kotlin.srcDirs = test.java.srcDirs = ['test'] | |||
main.resources.srcDirs = ['resources'] | |||
test.resources.srcDirs = ['testresources'] | |||
} | |||
dependencies { | |||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" | |||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" | |||
} |
@@ -0,0 +1,56 @@ | |||
package me.jfenn.audio | |||
import me.jfenn.audio.impl.JavaxMidiInterface | |||
import javax.sound.midi.* | |||
import kotlinx.coroutines.* | |||
import kotlinx.coroutines.flow.* | |||
import me.jfenn.audio.model.Note | |||
class AudioManager( | |||
val scope: CoroutineScope = GlobalScope | |||
) { | |||
val midi = JavaxMidiInterface() | |||
suspend fun start(block: suspend AudioManager.() -> Unit) { | |||
block() | |||
} | |||
fun metronome(interval: Long) : Flow<Int> = flow { | |||
var i = 0 | |||
while (true) { | |||
delay(interval) | |||
emit(i++) | |||
} | |||
} | |||
fun <T> Flow<T>.every(vararg count: Int, offset: Int = 0) : Flow<T> { | |||
var i = 0 | |||
var wait = 1 + offset | |||
return transform { value -> | |||
if (--wait == 0) { | |||
i = (i+1) % count.size | |||
wait = count[i] | |||
emit(value) | |||
} | |||
} | |||
} | |||
fun Flow<Note>.output() { | |||
scope.launch { | |||
collect { | |||
launch { | |||
delay(it.delay) | |||
midi.noteOn(it.instrument, it.number, it.velocity) | |||
delay(it.duration) | |||
midi.noteOff(it.instrument, it.number) | |||
} | |||
} | |||
} | |||
} | |||
fun destroy() { | |||
midi.destroy() | |||
} | |||
} |
@@ -0,0 +1,38 @@ | |||
package me.jfenn.audio | |||
import kotlinx.coroutines.flow.transform | |||
import kotlinx.coroutines.runBlocking | |||
import me.jfenn.audio.constants.* | |||
import me.jfenn.audio.model.Note | |||
fun main() = runBlocking<Unit> { | |||
println("Hello world!") | |||
AudioManager(scope = this).start { | |||
val metro = metronome(500) | |||
metro.transform { | |||
emit(Note(HARPSICHORD, arrayOf(40, 47, 52, 59).random(), 250)) | |||
}.transform { note -> | |||
emit(note) | |||
emit(Note(HARPSICHORD, note.number + 5, 250, delay = 250)) | |||
emit(Note(HARPSICHORD, note.number + 9, 250, delay = 500)) | |||
}.transform { note -> | |||
emit(note.copy(velocity = 80)) | |||
}.output() | |||
metro.every(8).transform { | |||
emit(Note(50, 68, 3000)) | |||
emit(Note(50, 59, 3100)) | |||
emit(Note(50, arrayOf(49, 51, 52).random(), 3200)) | |||
}.output() | |||
metro.every(4).transform { | |||
emit(Note(VIBRAPHONE, 28, 1500, 100)) | |||
}.output() | |||
metro.every(2, offset = 1).transform { | |||
emit(Note(128, 44, 100)) | |||
}.output() | |||
} | |||
} |
@@ -0,0 +1,19 @@ | |||
package me.jfenn.audio.constants | |||
const val ACOUSTIC_GRAND_PIANO = 1 | |||
const val BRIGHT_ACOUSTIC_PIANO = 2 | |||
const val ELECTRIC_GRAND_PIANO = 3 | |||
const val HONKY_TONK_PIANO = 4 | |||
const val ELECTRIC_PIANO_1 = 5 | |||
const val ELECTRIC_PIANO_2 = 6 | |||
const val HARPSICHORD = 7 | |||
const val CLAVI = 8 | |||
const val CELESTA = 9 | |||
const val GLOCKENSPIEL = 10 | |||
const val MUSIC_BOX = 11 | |||
const val VIBRAPHONE = 11 | |||
const val ACOUSTIC_BASS = 33 | |||
const val SYNTH_DRUM = 119 |
@@ -0,0 +1,63 @@ | |||
package me.jfenn.audio.impl | |||
import kotlinx.coroutines.delay | |||
import javax.sound.midi.MidiSystem | |||
const val MIDI_DELAY = 500L | |||
class JavaxMidiInterface() { | |||
val synth = MidiSystem.getSynthesizer().apply { open() } | |||
val instrumentMap = HashMap<Int, Int>() | |||
val noteMap = HashMap<Int, Int>() | |||
val usageMap = HashMap<Int, Int>() | |||
suspend fun noteOn(instrument: Int, number: Int, velocity: Int) { | |||
for (channel in (0..15)) if (instrumentMap[channel]?.equals(instrument) != false) { | |||
// map channel to instrument | |||
instrumentMap[channel] = instrument | |||
// add one usage to channel | |||
usageMap[channel] = (usageMap[channel] ?: 0) + 1 | |||
// add one usage to note | |||
val noteId = (number * 16) + channel | |||
noteMap[noteId] = (noteMap[noteId] ?: 0) + 1 | |||
// play the note | |||
synth.channels[channel].apply { | |||
if (program != instrument) | |||
programChange(instrument) | |||
delay(MIDI_DELAY) | |||
noteOn(number, velocity) | |||
} | |||
break | |||
} | |||
} | |||
suspend fun noteOff(instrument: Int, number: Int) { | |||
for (channel in (0..15)) if (instrumentMap[channel] == instrument) { | |||
// subtract one from channel usage | |||
usageMap[channel] = (usageMap[channel] ?: 1) - 1 | |||
// if usage == 0, un-map channel | |||
if (usageMap[channel] == 0) instrumentMap.remove(channel) | |||
// subtract one from note usage | |||
val noteId = (number * 16) + channel | |||
noteMap[noteId] = (noteMap[noteId] ?: 1) - 1 | |||
// release the note | |||
if (noteMap[noteId] == 0) { | |||
//delay(MIDI_DELAY) | |||
synth.channels[channel].noteOff(number) | |||
} | |||
break | |||
} | |||
} | |||
fun destroy() { | |||
synth.close() | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
package me.jfenn.audio.model | |||
data class Note( | |||
val instrument: Int, | |||
val number: Int, | |||
val duration: Long, | |||
val velocity: Int = 90, | |||
val delay: Long = 0 | |||
) |
@@ -0,0 +1,30 @@ | |||
// Top-level build file where you can add configuration options common to all sub-projects/modules. | |||
buildscript { | |||
ext.kotlin_version = '1.4.0' | |||
ext.coroutines_version = '1.4.2' | |||
ext.serialization_version = '0.20.0' | |||
repositories { | |||
google() | |||
jcenter() | |||
maven { url "https://kotlin.bintray.com/kotlinx" } | |||
maven { url "https://plugins.gradle.org/m2/" } | |||
} | |||
dependencies { | |||
classpath 'com.android.tools.build:gradle:4.0.0' | |||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" | |||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" | |||
// NOTE: Do not place your application dependencies here; they belong | |||
// in the individual module build.gradle files | |||
} | |||
} | |||
allprojects { | |||
repositories { | |||
google() | |||
jcenter() | |||
mavenCentral() | |||
} | |||
} |
@@ -0,0 +1,18 @@ | |||
apply plugin: 'kotlin' | |||
apply plugin: 'application' | |||
group 'me.jfenn' | |||
version '0.0.1' | |||
sourceSets { | |||
main.kotlin.srcDirs = main.java.srcDirs = ['src'] | |||
test.kotlin.srcDirs = test.java.srcDirs = ['test'] | |||
main.resources.srcDirs = ['resources'] | |||
test.resources.srcDirs = ['testresources'] | |||
} | |||
dependencies { | |||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" | |||
implementation project(':audio-automation') | |||
} |
@@ -0,0 +1,9 @@ | |||
ktor { | |||
deployment { | |||
port = 8080 | |||
port = ${?PORT} | |||
} | |||
application { | |||
modules = [ me.jfenn.ApplicationKt.module ] | |||
} | |||
} |
@@ -0,0 +1,12 @@ | |||
<configuration> | |||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> | |||
<encoder> | |||
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> | |||
</encoder> | |||
</appender> | |||
<root level="trace"> | |||
<appender-ref ref="STDOUT"/> | |||
</root> | |||
<logger name="org.eclipse.jetty" level="INFO"/> | |||
<logger name="io.netty" level="INFO"/> | |||
</configuration> |
@@ -0,0 +1,6 @@ | |||
package me.jfenn | |||
fun main(args: Array<String>) : Unit { | |||
println("Hello world!") | |||
} | |||
@@ -0,0 +1,4 @@ | |||
ktor_version=1.3.2 | |||
kotlin.code.style=official | |||
kotlin_version=1.3.72 | |||
logback_version=1.2.1 |
@@ -0,0 +1,5 @@ | |||
distributionBase=GRADLE_USER_HOME | |||
distributionPath=wrapper/dists | |||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-bin.zip | |||
zipStoreBase=GRADLE_USER_HOME | |||
zipStorePath=wrapper/dists |
@@ -0,0 +1,188 @@ | |||
#!/usr/bin/env sh | |||
# | |||
# Copyright 2015 the original author or authors. | |||
# | |||
# Licensed under the Apache License, Version 2.0 (the "License"); | |||
# you may not use this file except in compliance with the License. | |||
# You may obtain a copy of the License at | |||
# | |||
# https://www.apache.org/licenses/LICENSE-2.0 | |||
# | |||
# Unless required by applicable law or agreed to in writing, software | |||
# distributed under the License is distributed on an "AS IS" BASIS, | |||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
# See the License for the specific language governing permissions and | |||
# limitations under the License. | |||
# | |||
############################################################################## | |||
## | |||
## Gradle start up script for UN*X | |||
## | |||
############################################################################## | |||
# Attempt to set APP_HOME | |||
# Resolve links: $0 may be a link | |||
PRG="$0" | |||
# Need this for relative symlinks. | |||
while [ -h "$PRG" ] ; do | |||
ls=`ls -ld "$PRG"` | |||
link=`expr "$ls" : '.*-> \(.*\)$'` | |||
if expr "$link" : '/.*' > /dev/null; then | |||
PRG="$link" | |||
else | |||
PRG=`dirname "$PRG"`"/$link" | |||
fi | |||
done | |||
SAVED="`pwd`" | |||
cd "`dirname \"$PRG\"`/" >/dev/null | |||
APP_HOME="`pwd -P`" | |||
cd "$SAVED" >/dev/null | |||
APP_NAME="Gradle" | |||
APP_BASE_NAME=`basename "$0"` | |||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | |||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' | |||
# Use the maximum available, or set MAX_FD != -1 to use that value. | |||
MAX_FD="maximum" | |||
warn () { | |||
echo "$*" | |||
} | |||
die () { | |||
echo | |||
echo "$*" | |||
echo | |||
exit 1 | |||
} | |||
# OS specific support (must be 'true' or 'false'). | |||
cygwin=false | |||
msys=false | |||
darwin=false | |||
nonstop=false | |||
case "`uname`" in | |||
CYGWIN* ) | |||
cygwin=true | |||
;; | |||
Darwin* ) | |||
darwin=true | |||
;; | |||
MINGW* ) | |||
msys=true | |||
;; | |||
NONSTOP* ) | |||
nonstop=true | |||
;; | |||
esac | |||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar | |||
# Determine the Java command to use to start the JVM. | |||
if [ -n "$JAVA_HOME" ] ; then | |||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then | |||
# IBM's JDK on AIX uses strange locations for the executables | |||
JAVACMD="$JAVA_HOME/jre/sh/java" | |||
else | |||
JAVACMD="$JAVA_HOME/bin/java" | |||
fi | |||
if [ ! -x "$JAVACMD" ] ; then | |||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME | |||
Please set the JAVA_HOME variable in your environment to match the | |||
location of your Java installation." | |||
fi | |||
else | |||
JAVACMD="java" | |||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. | |||
Please set the JAVA_HOME variable in your environment to match the | |||
location of your Java installation." | |||
fi | |||
# Increase the maximum file descriptors if we can. | |||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then | |||
MAX_FD_LIMIT=`ulimit -H -n` | |||
if [ $? -eq 0 ] ; then | |||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then | |||
MAX_FD="$MAX_FD_LIMIT" | |||
fi | |||
ulimit -n $MAX_FD | |||
if [ $? -ne 0 ] ; then | |||
warn "Could not set maximum file descriptor limit: $MAX_FD" | |||
fi | |||
else | |||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" | |||
fi | |||
fi | |||
# For Darwin, add options to specify how the application appears in the dock | |||
if $darwin; then | |||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" | |||
fi | |||
# For Cygwin or MSYS, switch paths to Windows format before running java | |||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then | |||
APP_HOME=`cygpath --path --mixed "$APP_HOME"` | |||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` | |||
JAVACMD=`cygpath --unix "$JAVACMD"` | |||
# We build the pattern for arguments to be converted via cygpath | |||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` | |||
SEP="" | |||
for dir in $ROOTDIRSRAW ; do | |||
ROOTDIRS="$ROOTDIRS$SEP$dir" | |||
SEP="|" | |||
done | |||
OURCYGPATTERN="(^($ROOTDIRS))" | |||
# Add a user-defined pattern to the cygpath arguments | |||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then | |||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" | |||
fi | |||
# Now convert the arguments - kludge to limit ourselves to /bin/sh | |||
i=0 | |||
for arg in "$@" ; do | |||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` | |||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option | |||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition | |||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` | |||
else | |||
eval `echo args$i`="\"$arg\"" | |||
fi | |||
i=$((i+1)) | |||
done | |||
case $i in | |||
(0) set -- ;; | |||
(1) set -- "$args0" ;; | |||
(2) set -- "$args0" "$args1" ;; | |||
(3) set -- "$args0" "$args1" "$args2" ;; | |||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;; | |||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; | |||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; | |||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; | |||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; | |||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; | |||
esac | |||
fi | |||
# Escape application args | |||
save () { | |||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done | |||
echo " " | |||
} | |||
APP_ARGS=$(save "$@") | |||
# Collect all arguments for the java command, following the shell quoting and substitution rules | |||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" | |||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong | |||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then | |||
cd "$(dirname "$0")" | |||
fi | |||
exec "$JAVACMD" "$@" |
@@ -0,0 +1,100 @@ | |||
@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 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 |
@@ -0,0 +1,4 @@ | |||
rootProject.name = "audio-automation" | |||
enableFeaturePreview('GRADLE_METADATA') | |||
include ':audio-automation' | |||
include ':example' |