From 9de800fa4bc6627fc24f5926a1366f856f1a2481 Mon Sep 17 00:00:00 2001 From: Kazik Pogoda Date: Thu, 30 Jun 2022 18:49:32 +0200 Subject: [PATCH 1/8] Initial Looper interface --- src/commonMain/kotlin/WesternMusic.kt | 20 +++++ src/commonMain/kotlin/loop/MidiLooper.kt | 102 +++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 src/commonMain/kotlin/WesternMusic.kt create mode 100644 src/commonMain/kotlin/loop/MidiLooper.kt diff --git a/src/commonMain/kotlin/WesternMusic.kt b/src/commonMain/kotlin/WesternMusic.kt new file mode 100644 index 0000000..4e338f1 --- /dev/null +++ b/src/commonMain/kotlin/WesternMusic.kt @@ -0,0 +1,20 @@ +/* + * xemantic-music - a Kotlin library implementing some theory of music and composition + * Copyright (C) 2022 Kazimierz Pogoda + * + * This file is part of xemantic-music. + * + * xemantic-music is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * xemantic-music is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with xemantic-music. + * If not, see . + */ + +package com.xemantic.music + diff --git a/src/commonMain/kotlin/loop/MidiLooper.kt b/src/commonMain/kotlin/loop/MidiLooper.kt new file mode 100644 index 0000000..dc09a11 --- /dev/null +++ b/src/commonMain/kotlin/loop/MidiLooper.kt @@ -0,0 +1,102 @@ +/* + * xemantic-music - a Kotlin library implementing some theory of music and composition + * Copyright (C) 2022 Kazimierz Pogoda + * + * This file is part of xemantic-music. + * + * xemantic-music is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * xemantic-music is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with xemantic-music. + * If not, see . + */ + +package com.xemantic.music.loop + +/** + * A looper pedal-like API. It's a general interface but focused on MIDI devices. + */ +interface Looper { + + /** + * The list of recordings sorted by the start time. + */ + val recordings: List + + /** + * The list of loops being currently played, sorted by the start time. + */ + val playedLoops: List + + /** + * Indicates if something is being currently recorded. + */ + val recording: Boolean + + /** + * Starts recording under a given `name`. + * The [Recording] will be created only if [stopRecording] is being called afterwards. + * + * @param name the name of the recording. + * @throws IllegalArgumentException if the recording of given name already exists. + * @throws IllegalStateException if the looper is currently recording. + */ + fun startRecording(name: String) + + /** + * Stops the recording initiated with [startRecording]. + * + * @return the [Recording] instance describing the finished recording. + * @throws IllegalStateException if nothing is being recorded. See [recording] flog. + */ + fun stopRecording(): Recording + + /** + * Plays the loop. + * + * @param name the name of the loop. + * @param repetitionCount how many times it should be repeated, `-1` by default which + * implies endless playback (at least until [stopLoop] is called. + * @param onLoopStopped an optional callback called when the loop is stopped. Either + * triggered after reaching `repetitionCount` or when [stopLoop] is being called. + * @return the loop id. + */ + fun playLoop( + name: String, + repetitionCount: Int = -1, + onLoopStopped: (Int) -> Unit = {} + ): Int + + /** + * Stops the loop of given id. + * Note: it will cause the loop to be removed from [playedLoops]. + * + * @param loopId the loop id. + */ + fun stopLoop(loopId: Int) + + /** + * Removes recording of given `name`. + * + * @param name the name of the recording to remove. + */ + fun removeRecording(name: String) + +} + +data class Recording( + val name: String, + val start: Long, + val stop: Long +) + +data class Loop( + val id: Int, + val start: Long, + val recording: Recording +) From aa607efd4b06d9cb37b7dd2a7683be12cb0d2161 Mon Sep 17 00:00:00 2001 From: Kazik Pogoda Date: Thu, 30 Jun 2022 20:27:26 +0200 Subject: [PATCH 2/8] playedLoops renamed to playingLoops --- src/commonMain/kotlin/loop/{MidiLooper.kt => Looper.kt} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/commonMain/kotlin/loop/{MidiLooper.kt => Looper.kt} (96%) diff --git a/src/commonMain/kotlin/loop/MidiLooper.kt b/src/commonMain/kotlin/loop/Looper.kt similarity index 96% rename from src/commonMain/kotlin/loop/MidiLooper.kt rename to src/commonMain/kotlin/loop/Looper.kt index dc09a11..98a71c0 100644 --- a/src/commonMain/kotlin/loop/MidiLooper.kt +++ b/src/commonMain/kotlin/loop/Looper.kt @@ -31,7 +31,7 @@ interface Looper { /** * The list of loops being currently played, sorted by the start time. */ - val playedLoops: List + val playingLoops: List /** * Indicates if something is being currently recorded. @@ -74,7 +74,7 @@ interface Looper { /** * Stops the loop of given id. - * Note: it will cause the loop to be removed from [playedLoops]. + * Note: it will cause the loop to be removed from [playingLoops]. * * @param loopId the loop id. */ From a7c8406a1a219997ea2f567dc66c25a83ae5ccce Mon Sep 17 00:00:00 2001 From: Kazik Pogoda Date: Thu, 30 Jun 2022 22:57:19 +0200 Subject: [PATCH 3/8] initial Java version of the Looper --- build.gradle.kts | 15 +- src/commonMain/kotlin/loop/Looper.kt | 2 + src/jvmMain/kotlin/JvmMusic.kt | 19 ++ src/jvmMain/kotlin/loop/JavaMidiLooper.kt | 192 ++++++++++++++++++ src/jvmTest/kotlin/JvmMusicTest.kt | 20 ++ .../kotlin/loop/JavaMidiLooperSwingUi.kt | 85 ++++++++ src/jvmTest/resources/log4j2.yaml | 21 ++ 7 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 src/jvmMain/kotlin/JvmMusic.kt create mode 100644 src/jvmMain/kotlin/loop/JavaMidiLooper.kt create mode 100644 src/jvmTest/kotlin/JvmMusicTest.kt create mode 100644 src/jvmTest/kotlin/loop/JavaMidiLooperSwingUi.kt create mode 100644 src/jvmTest/resources/log4j2.yaml diff --git a/build.gradle.kts b/build.gradle.kts index 8455e7d..b42253d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,6 +9,8 @@ repositories { mavenCentral() } +val log4jVersion = "2.17.2" + kotlin { jvm { compilations.all { @@ -42,8 +44,17 @@ kotlin { implementation(kotlin("test")) } } - val jvmMain by getting - val jvmTest by getting + val jvmMain by getting { + dependencies { + implementation("org.apache.logging.log4j:log4j-api:$log4jVersion") + } + } + val jvmTest by getting { + dependencies { + implementation("org.apache.logging.log4j:log4j-core:$log4jVersion") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.3") + } + } val jsMain by getting val jsTest by getting val nativeMain by getting diff --git a/src/commonMain/kotlin/loop/Looper.kt b/src/commonMain/kotlin/loop/Looper.kt index 98a71c0..062c96f 100644 --- a/src/commonMain/kotlin/loop/Looper.kt +++ b/src/commonMain/kotlin/loop/Looper.kt @@ -77,6 +77,7 @@ interface Looper { * Note: it will cause the loop to be removed from [playingLoops]. * * @param loopId the loop id. + * @throws IllegalArgumentException if the loop does not exists. */ fun stopLoop(loopId: Int) @@ -84,6 +85,7 @@ interface Looper { * Removes recording of given `name`. * * @param name the name of the recording to remove. + * @throws IllegalArgumentException if the recording of given name does not exists. */ fun removeRecording(name: String) diff --git a/src/jvmMain/kotlin/JvmMusic.kt b/src/jvmMain/kotlin/JvmMusic.kt new file mode 100644 index 0000000..19c2153 --- /dev/null +++ b/src/jvmMain/kotlin/JvmMusic.kt @@ -0,0 +1,19 @@ +/* + * xemantic-music - a Kotlin library implementing some theory of music and composition + * Copyright (C) 2022 Kazimierz Pogoda + * + * This file is part of xemantic-music. + * + * xemantic-music is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * xemantic-music is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with xemantic-music. + * If not, see . + */ + +package com.xemantic.music diff --git a/src/jvmMain/kotlin/loop/JavaMidiLooper.kt b/src/jvmMain/kotlin/loop/JavaMidiLooper.kt new file mode 100644 index 0000000..8b1efff --- /dev/null +++ b/src/jvmMain/kotlin/loop/JavaMidiLooper.kt @@ -0,0 +1,192 @@ +/* + * xemantic-music - a Kotlin library implementing some theory of music and composition + * Copyright (C) 2022 Kazimierz Pogoda + * + * This file is part of xemantic-music. + * + * xemantic-music is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * xemantic-music is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with xemantic-music. + * If not, see . + */ + +package com.xemantic.music.loop + +import org.apache.logging.log4j.LogManager +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference +import javax.sound.midi.MidiDevice +import javax.sound.midi.MidiSystem +import javax.sound.midi.Sequence +import javax.sound.midi.Sequencer + +class JavaMidiLooper( + private val input: MidiDevice, + private val output: MidiDevice +) : Looper, AutoCloseable { + + private val logger = LogManager.getLogger() + + private val recordingSequencer = MidiSystem.getSequencer(false) + + init { + if (!input.isOpen) { + input.open() + } + if (!output.isOpen) { + output.open() + } + recordingSequencer.open() + } + + private val recordingMap = ConcurrentHashMap>() + + private val recordingState = AtomicBoolean(false) + + private val startedRecordingRef = AtomicReference() + + private val loopMap = ConcurrentHashMap() + + private val loopCounter = AtomicInteger(0) + + override val recordings: List get() = recordingMap.values.map { it.first } + + override val playingLoops: List get() = loopMap.values.map { it.loop } + + override val recording: Boolean get() = recordingState.get() + + override fun startRecording(name: String) { + + if (recordingState.getAndSet(true)) { + throw IllegalStateException("Cannot start new recording if already recording") + } + + if (recordingMap.containsKey(name)) { + throw IllegalArgumentException("Recording already exists: $name") + } + + logger.info("startRecording: $name") + + input.transmitter.receiver = recordingSequencer.receiver + with (recordingSequencer) { + sequence = Sequence(Sequence.PPQ, 24) // TODO where to set up the resolution? + val track = sequence.createTrack() + recordEnable(track, -1) + tickPosition = recordingSequencer.tickPosition + startRecording() + } + + startedRecordingRef.set( + StartedRecording( + name = name, + start = System.currentTimeMillis() + ) + ) + + } + + override fun stopRecording(): Recording { + + if (!recordingState.get()) { + throw IllegalStateException("Cannot stop recording which was not started") + } + + val startedRecording = startedRecordingRef.getAndSet(null) + + logger.info("stopRecording: ${startedRecording.name}") + + input.transmitter.receiver = null // TODO is that correct, or rather close? + //Thread.sleep(100) + //inputSequencer.receiver.close() + recordingSequencer.stopRecording() + //inputSequencer.stop() + val recording = Recording( + name = startedRecording.name, + start = startedRecording.start, + stop = System.currentTimeMillis() + ) + recordingMap[startedRecording.name] = Pair(recording, recordingSequencer.sequence) + recordingState.set(false) + return recording + } + + override fun playLoop( + name: String, + repetitionCount: Int, + onLoopStopped: (Int) -> Unit + ): Int { + + val recordingPair = recordingMap[name] + ?: throw IllegalArgumentException("Cannot play loop which does not exist: $name") + + val loopId = loopCounter.incrementAndGet() + + // TODO add logging infinity + logger.info("Playing loop, recording: $name, repetitions: $repetitionCount") + + val sequencer = MidiSystem.getSequencer(false).apply { + open() + transmitter.receiver = output.receiver + sequence = recordingPair.second + loopCount = repetitionCount + addMetaEventListener { + logger.info("metaEvent: $it, type: ${it.type}") + if (it.type == 47) { + close()// end of sequence + loopMap.remove(loopId) + } + } + start() + } + + loopMap[loopId] = PlayingLoop( + loop = Loop( + id = loopId, + start = System.currentTimeMillis(), + recording = recordingPair.first + ), + sequencer = sequencer + ) + + return loopId + } + + override fun stopLoop(loopId: Int) { + logger.info("Stopping loop: $loopId") + val loop = loopMap.remove(loopId) + ?: throw IllegalArgumentException("Cannot stop non-existent loop: $loopId") + loop.sequencer.stop() + } + + override fun removeRecording(name: String) { + logger.info("Removing recording: $name") + if (recordingMap.remove(name) == null) { + throw IllegalArgumentException("Cannot remove non-existent recording: $name") + } + } + + override fun close() { + logger.info("Closing resources") + input.transmitter.receiver = null + recordingSequencer.close() + } + +} + +internal data class StartedRecording( + val name: String, + val start: Long +) + +internal data class PlayingLoop( + val loop: Loop, + val sequencer: Sequencer +) diff --git a/src/jvmTest/kotlin/JvmMusicTest.kt b/src/jvmTest/kotlin/JvmMusicTest.kt new file mode 100644 index 0000000..4e338f1 --- /dev/null +++ b/src/jvmTest/kotlin/JvmMusicTest.kt @@ -0,0 +1,20 @@ +/* + * xemantic-music - a Kotlin library implementing some theory of music and composition + * Copyright (C) 2022 Kazimierz Pogoda + * + * This file is part of xemantic-music. + * + * xemantic-music is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * xemantic-music is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with xemantic-music. + * If not, see . + */ + +package com.xemantic.music + diff --git a/src/jvmTest/kotlin/loop/JavaMidiLooperSwingUi.kt b/src/jvmTest/kotlin/loop/JavaMidiLooperSwingUi.kt new file mode 100644 index 0000000..bd19f10 --- /dev/null +++ b/src/jvmTest/kotlin/loop/JavaMidiLooperSwingUi.kt @@ -0,0 +1,85 @@ +/* + * xemantic-music - a Kotlin library implementing some theory of music and composition + * Copyright (C) 2022 Kazimierz Pogoda + * + * This file is part of xemantic-music. + * + * xemantic-music is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * xemantic-music is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with xemantic-music. + * If not, see . + */ + +package com.xemantic.music.loop + +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import java.awt.FlowLayout +import javax.sound.midi.MidiSystem +import javax.swing.* + +val logger: Logger = LogManager.getLogger() + +fun main() { + + val midiInfos = MidiSystem.getMidiDeviceInfo() + logger.info("MIDI devices") + midiInfos.forEach { + logger.info(" |-name: '${it.name}', vendor: '${it.vendor}', description: '${it.description}', version: '${it.version}'") + } + val midiNameString = "Piano" + val keyboardOutputInfo = midiInfos.first { it.name.contains(midiNameString) } + val keyboardInputInfo = midiInfos.last { it.name.contains(midiNameString) } + + val looper = JavaMidiLooper( + MidiSystem.getMidiDevice(keyboardInputInfo), + MidiSystem.getMidiDevice(keyboardOutputInfo) + ) + + SwingUtilities.invokeLater { + var sequenceCounter = 0 + JFrame().apply { + defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE + contentPane = JPanel(FlowLayout()) + contentPane.add( + JButton("record: $sequenceCounter").apply { + addActionListener { + if (looper.recording) { + val sequenceName = sequenceCounter.toString() + looper.stopRecording() + contentPane.add(JButton("loop: $sequenceName").apply { + addActionListener { + val loopNumber = looper.playLoop(sequenceName) + contentPane.add(JButton("stop loop: $sequenceName - $loopNumber ").apply { + addActionListener { + looper.stopLoop(loopNumber) + remove(this) + pack() + } + }) + pack() + } + }) + sequenceCounter++ + text = "record: $sequenceName" + pack() + } else { + val sequenceName = sequenceCounter.toString() + looper.startRecording(sequenceName) + text = "stop recording: $sequenceName" + pack() + } + } + }) + pack() + isVisible = true + } + } + +} diff --git a/src/jvmTest/resources/log4j2.yaml b/src/jvmTest/resources/log4j2.yaml new file mode 100644 index 0000000..2027908 --- /dev/null +++ b/src/jvmTest/resources/log4j2.yaml @@ -0,0 +1,21 @@ +Configuration: + status: info + Appenders: + Console: + - name: Console_Info + target: SYSTEM_ERR + PatternLayout: + Pattern: "%highlight{${LOG_LEVEL_PATTERN:-%5p}}{FATAL=red, ERROR=red, WARN=yellow, INFO=green, DEBUG=green, TRACE=green} %style{[%t]}{white} %style{%-30.30c{1.}}{white} %style{ ↘ %m%n%ex}{white}" + #Pattern: "%style{%d{yyyy-MM-dd HH:mm:ss.SSS}}{white} %highlight{${LOG_LEVEL_PATTERN:-%5p}}{FATAL=red, ERROR=red, WARN=yellow, INFO=green, DEBUG=green, TRACE=green} %style{[%t]}{white} %style{%-30.30c{1.}}{cyan} %style{:%m%n%ex}{white}" +# File: +# append: false +# name: File_Appender +# fileName: application.log +# PatternLayout: +# Pattern: "%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" + Loggers: + Root: + level: info + AppenderRef: + - ref: Console_Info + - ref: File_Appender From 579a49100366994a203edd92574ae1c14be7fc6e Mon Sep 17 00:00:00 2001 From: Kazik Pogoda Date: Fri, 1 Jul 2022 17:22:27 +0200 Subject: [PATCH 4/8] bug fixes for recording timing --- src/commonMain/kotlin/loop/Looper.kt | 5 ++- src/jvmMain/kotlin/loop/JavaMidiLooper.kt | 41 ++++++++++++++--------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/commonMain/kotlin/loop/Looper.kt b/src/commonMain/kotlin/loop/Looper.kt index 062c96f..824468a 100644 --- a/src/commonMain/kotlin/loop/Looper.kt +++ b/src/commonMain/kotlin/loop/Looper.kt @@ -41,12 +41,15 @@ interface Looper { /** * Starts recording under a given `name`. * The [Recording] will be created only if [stopRecording] is being called afterwards. + * Note: if any loop is being played, the recording will stop immediately at the end + * of the first played loop. * * @param name the name of the recording. + * @param onStopRecording an optional parameter to describe what happens when recording is finished. * @throws IllegalArgumentException if the recording of given name already exists. * @throws IllegalStateException if the looper is currently recording. */ - fun startRecording(name: String) + fun startRecording(name: String, onStopRecording: (Recording) -> Unit = {}) // TODO it should automatically /** * Stops the recording initiated with [startRecording]. diff --git a/src/jvmMain/kotlin/loop/JavaMidiLooper.kt b/src/jvmMain/kotlin/loop/JavaMidiLooper.kt index 8b1efff..3ea85bd 100644 --- a/src/jvmMain/kotlin/loop/JavaMidiLooper.kt +++ b/src/jvmMain/kotlin/loop/JavaMidiLooper.kt @@ -38,16 +38,13 @@ class JavaMidiLooper( private val recordingSequencer = MidiSystem.getSequencer(false) init { - if (!input.isOpen) { - input.open() - } if (!output.isOpen) { output.open() } recordingSequencer.open() } - private val recordingMap = ConcurrentHashMap>() + private val midiRecordingMap = ConcurrentHashMap() private val recordingState = AtomicBoolean(false) @@ -57,19 +54,19 @@ class JavaMidiLooper( private val loopCounter = AtomicInteger(0) - override val recordings: List get() = recordingMap.values.map { it.first } + override val recordings: List get() = midiRecordingMap.values.map { it.recording } override val playingLoops: List get() = loopMap.values.map { it.loop } override val recording: Boolean get() = recordingState.get() - override fun startRecording(name: String) { + override fun startRecording(name: String, onStopRecording: (Recording) -> Unit) { if (recordingState.getAndSet(true)) { throw IllegalStateException("Cannot start new recording if already recording") } - if (recordingMap.containsKey(name)) { + if (midiRecordingMap.containsKey(name)) { throw IllegalArgumentException("Recording already exists: $name") } @@ -77,12 +74,14 @@ class JavaMidiLooper( input.transmitter.receiver = recordingSequencer.receiver with (recordingSequencer) { + //tickPosition = 0 sequence = Sequence(Sequence.PPQ, 24) // TODO where to set up the resolution? val track = sequence.createTrack() recordEnable(track, -1) - tickPosition = recordingSequencer.tickPosition - startRecording() + //microsecondPosition = input.microsecondPosition } + input.open() + recordingSequencer.startRecording() startedRecordingRef.set( StartedRecording( @@ -90,7 +89,7 @@ class JavaMidiLooper( start = System.currentTimeMillis() ) ) - + logger.info("First tick: ${recordingSequencer.tickPosition}") } override fun stopRecording(): Recording { @@ -103,18 +102,23 @@ class JavaMidiLooper( logger.info("stopRecording: ${startedRecording.name}") - input.transmitter.receiver = null // TODO is that correct, or rather close? + //input.transmitter.receiver = null // TODO is that correct, or rather close? //Thread.sleep(100) //inputSequencer.receiver.close() recordingSequencer.stopRecording() + input.close() //inputSequencer.stop() val recording = Recording( name = startedRecording.name, start = startedRecording.start, stop = System.currentTimeMillis() ) - recordingMap[startedRecording.name] = Pair(recording, recordingSequencer.sequence) + midiRecordingMap[startedRecording.name] = MidiRecording( + recording = recording, + sequence = recordingSequencer.sequence + ) recordingState.set(false) + recordingSequencer.sequence = null return recording } @@ -124,7 +128,7 @@ class JavaMidiLooper( onLoopStopped: (Int) -> Unit ): Int { - val recordingPair = recordingMap[name] + val midiRecording = midiRecordingMap[name] ?: throw IllegalArgumentException("Cannot play loop which does not exist: $name") val loopId = loopCounter.incrementAndGet() @@ -135,7 +139,7 @@ class JavaMidiLooper( val sequencer = MidiSystem.getSequencer(false).apply { open() transmitter.receiver = output.receiver - sequence = recordingPair.second + sequence = midiRecording.sequence loopCount = repetitionCount addMetaEventListener { logger.info("metaEvent: $it, type: ${it.type}") @@ -151,7 +155,7 @@ class JavaMidiLooper( loop = Loop( id = loopId, start = System.currentTimeMillis(), - recording = recordingPair.first + recording = midiRecording.recording ), sequencer = sequencer ) @@ -168,7 +172,7 @@ class JavaMidiLooper( override fun removeRecording(name: String) { logger.info("Removing recording: $name") - if (recordingMap.remove(name) == null) { + if (midiRecordingMap.remove(name) == null) { throw IllegalArgumentException("Cannot remove non-existent recording: $name") } } @@ -190,3 +194,8 @@ internal data class PlayingLoop( val loop: Loop, val sequencer: Sequencer ) + +internal data class MidiRecording( + val recording: Recording, + val sequence: Sequence +) From 11a6658216190a1db74b5c7e5f98ba98b8a87fe9 Mon Sep 17 00:00:00 2001 From: Kazik Pogoda Date: Mon, 6 Nov 2023 19:16:27 +0100 Subject: [PATCH 5/8] build files and versions updated --- build.gradle.kts | 78 ++++++++++++++++++++++----------------- gradle.properties | 3 +- gradle/libs.versions.toml | 41 ++++++++++++++++++++ 3 files changed, 88 insertions(+), 34 deletions(-) create mode 100644 gradle/libs.versions.toml diff --git a/build.gradle.kts b/build.gradle.kts index b42253d..2d6febb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,64 +1,76 @@ plugins { - kotlin("multiplatform") version "1.7.0" + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.dokka) + alias(libs.plugins.gradle.versions.plugin) + `maven-publish` } -group = "com.xemantic.music" -version = "1.0-SNAPSHOT" - repositories { mavenCentral() } -val log4jVersion = "2.17.2" - kotlin { + + jvmToolchain(libs.versions.jvmTarget.get().toInt()) + jvm { - compilations.all { - kotlinOptions.jvmTarget = "15" - } - withJava() - testRuns["test"].executionTask.configure { - useJUnitPlatform() - } - } - js(IR) { - browser { - commonWebpackConfig { - cssSupport.enabled = true + compilations { + all { + kotlinOptions { + jvmTarget = libs.versions.jvmTarget.get() + } } } } + + js { + browser {} + } + val hostOs = System.getProperty("os.name") val isMingwX64 = hostOs.startsWith("Windows") + @Suppress("UNUSED_VARIABLE") val nativeTarget = when { - hostOs == "Mac OS X" -> macosX64("native") - hostOs == "Linux" -> linuxX64("native") - isMingwX64 -> mingwX64("native") + hostOs == "Mac OS X" -> macosX64() + hostOs == "Linux" -> linuxX64() + isMingwX64 -> mingwX64() else -> throw GradleException("Host OS is not supported in Kotlin/Native.") } sourceSets { - val commonMain by getting - val commonTest by getting { + + all { + languageSettings { + languageVersion = libs.versions.kotlinLanguageVersion.get() + apiVersion = libs.versions.kotlinLanguageVersion.get() + } + } + + commonMain { dependencies { - implementation(kotlin("test")) + api(libs.kotlin.coroutines) + api(libs.kotlin.datetime) + implementation(libs.kotlin.logging) } } - val jvmMain by getting { + + commonTest { dependencies { - implementation("org.apache.logging.log4j:log4j-api:$log4jVersion") + implementation(libs.kotlin.test) + implementation(libs.kotlin.coroutines.test) + implementation(libs.kotest.assertions.core) } } - val jvmTest by getting { + + jvmTest { dependencies { - implementation("org.apache.logging.log4j:log4j-core:$log4jVersion") - implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.3") + runtimeOnly(libs.log4j.slf4j2) + runtimeOnly(libs.log4j.core) + runtimeOnly(libs.jackson.databind) + runtimeOnly(libs.jackson.json) } } - val jsMain by getting - val jsTest by getting - val nativeMain by getting - val nativeTest by getting + } } diff --git a/gradle.properties b/gradle.properties index b61a798..62a0be7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,4 @@ kotlin.code.style=official kotlin.js.generate.executable.default=false -kotlin.mpp.stability.nowarn=true +group=com.xemantic.music +version=1.0-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..d44ff83 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,41 @@ +[versions] +# kotlin +kotlin = "1.9.20" +kotlinLanguageVersion = "1.9" +jvmTarget = "21" +kotlinCoroutines = "1.7.3" +kotlinDatetime = "0.4.1" + +# logging +kotlinLogging = "5.1.0" +slf4j = "2.0.7" +log4j = "2.20.0" +jackson = "2.15.2" + +# test +kotest = "5.8.0" + +# plugins +dokka = "1.9.10" +gradleVersionsPlugin = "0.49.0" + +[libraries] +kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinCoroutines" } +kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinCoroutines" } +kotlin-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinDatetime" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-test-annotations-common = { module = "org.jetbrains.kotlin:kotlin-test-annotations-common", version.ref = "kotlin" } + +kotlin-logging = { module = "io.github.oshai:kotlin-logging", version.ref = "kotlinLogging" } +slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" } +log4j-slf4j2 = { group = "org.apache.logging.log4j", name = "log4j-slf4j2-impl", version.ref = "log4j" } +log4j-core = { group = "org.apache.logging.log4j", name = "log4j-core", version.ref = "log4j" } +jackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jackson" } +jackson-json = { group = "com.fasterxml.jackson.dataformat", name = "jackson-dataformat-yaml", version.ref = "jackson" } + +kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } + +[plugins] +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } +gradle-versions-plugin = { id = "com.github.ben-manes.versions", version.ref = "gradleVersionsPlugin" } From a6a5620022e6834d65de80c380675e31736d65e3 Mon Sep 17 00:00:00 2001 From: Kazik Pogoda Date: Mon, 6 Nov 2023 19:39:01 +0100 Subject: [PATCH 6/8] circle of fifths --- src/commonMain/kotlin/CircleOfFifths.kt | 37 ++++++++++++++++++ src/commonTest/kotlin/CircleOfFifthsTest.kt | 42 +++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 src/commonMain/kotlin/CircleOfFifths.kt create mode 100644 src/commonTest/kotlin/CircleOfFifthsTest.kt diff --git a/src/commonMain/kotlin/CircleOfFifths.kt b/src/commonMain/kotlin/CircleOfFifths.kt new file mode 100644 index 0000000..a1042de --- /dev/null +++ b/src/commonMain/kotlin/CircleOfFifths.kt @@ -0,0 +1,37 @@ +/* + * xemantic-music - a Kotlin library implementing some theory of music and composition + * Copyright (C) 2023 Kazimierz Pogoda + * + * This file is part of xemantic-music. + * + * xemantic-music is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * xemantic-music is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with xemantic-music. + * If not, see . + */ + +package com.xemantic.music + +private val indexesInTheCircleOfFifths = arrayOf( + 0, // C + 7, // C# + 2, // D + 9, // D# / Eb + 4, // E + 11, // F + 6, // Gb / F# + 1, // G + 8, // Ab + 3, // A + 10, // Bb + 5 // B +) + +val Note.indexInTheCircleOfFifths: Int + get() = indexesInTheCircleOfFifths[indexInOctave] diff --git a/src/commonTest/kotlin/CircleOfFifthsTest.kt b/src/commonTest/kotlin/CircleOfFifthsTest.kt new file mode 100644 index 0000000..352ffc3 --- /dev/null +++ b/src/commonTest/kotlin/CircleOfFifthsTest.kt @@ -0,0 +1,42 @@ +/* + * xemantic-music - a Kotlin library implementing some theory of music and composition + * Copyright (C) 2023 Kazimierz Pogoda + * + * This file is part of xemantic-music. + * + * xemantic-music is free software: you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * xemantic-music is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with xemantic-music. + * If not, see . + */ + +package com.xemantic.music + +import io.kotest.matchers.shouldBe +import kotlin.test.Test + +class CircleOfFifthsTest { + + @Test + fun shouldReturnProperIndexInTheCircleOfFifths() { + Note.C.indexInTheCircleOfFifths shouldBe 0 + Note.Cs.indexInTheCircleOfFifths shouldBe 7 + Note.D.indexInTheCircleOfFifths shouldBe 2 + Note.Ds.indexInTheCircleOfFifths shouldBe 9 + Note.E.indexInTheCircleOfFifths shouldBe 4 + Note.F.indexInTheCircleOfFifths shouldBe 11 + Note.Fs.indexInTheCircleOfFifths shouldBe 6 + Note.G.indexInTheCircleOfFifths shouldBe 1 + Note.Gs.indexInTheCircleOfFifths shouldBe 8 + Note.A.indexInTheCircleOfFifths shouldBe 3 + Note.As.indexInTheCircleOfFifths shouldBe 10 + Note.B.indexInTheCircleOfFifths shouldBe 5 + } + +} From 6d1c8945270c6262714391f01b97c8d368d81ff8 Mon Sep 17 00:00:00 2001 From: Kazik Pogoda Date: Mon, 6 Nov 2023 19:39:17 +0100 Subject: [PATCH 7/8] gradle version update --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index aa991fc..e411586 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 66ecd92565d0803633efb9b434c02def695c1687 Mon Sep 17 00:00:00 2001 From: Kazik Pogoda Date: Thu, 9 Nov 2023 18:16:36 +0100 Subject: [PATCH 8/8] dependency updates --- build.gradle.kts | 21 ++++++++++++++++++++- gradle/libs.versions.toml | 4 ++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 2d6febb..28a4af8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,7 +23,7 @@ kotlin { } } - js { + js(IR) { browser {} } @@ -74,3 +74,22 @@ kotlin { } } + +tasks { + + dependencyUpdates { + gradleReleaseChannel = "current" + rejectVersionIf { + isNonStable(candidate.version) && !isNonStable(currentVersion) + } + } + +} + +private val nonStableKeywords = listOf("alpha", "beta", "rc") + +fun isNonStable( + version: String +): Boolean = nonStableKeywords.any { + version.lowercase().contains(it) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d44ff83..a01bb37 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,8 +9,8 @@ kotlinDatetime = "0.4.1" # logging kotlinLogging = "5.1.0" slf4j = "2.0.7" -log4j = "2.20.0" -jackson = "2.15.2" +log4j = "2.21.1" +jackson = "2.15.3" # test kotest = "5.8.0"