Skip to content

Commit 963bd92

Browse files
Reimplement in Java
This removes the dependency on the Kotlin stdlib.
1 parent 2c4a246 commit 963bd92

File tree

11 files changed

+488
-335
lines changed

11 files changed

+488
-335
lines changed

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
end_of_line = lf
33
insert_final_newline = true
44

5-
[*.{kt, kts, cpp}]
5+
[*.{java, cpp}]
66
indent_size = 4

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
[![GitHub (pre-)release](https://img.shields.io/github/release/BjoernPetersen/volctl/all.svg)](https://github.com/BjoernPetersen/volctl/releases) [![GitHub license](https://img.shields.io/github/license/BjoernPetersen/volctl.svg)](https://github.com/BjoernPetersen/volctl/blob/master/LICENSE)
44

5-
A simple Kotlin library providing access to audio volume control on Windows and Linux.
6-
Can also be used from Java.
5+
A simple Java library providing access to audio volume control on Windows and Linux.
76

87
The library uses native C++ code to directly access the relevant system APIs,
98
there are no further dependencies.

build.gradle.kts

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
import com.diffplug.spotless.LineEnding
2-
import org.jetbrains.dokka.gradle.DokkaTask
3-
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
42

53
plugins {
64
id("com.diffplug.gradle.spotless") version Plugin.SPOTLESS
7-
id("io.gitlab.arturbosch.detekt") version Plugin.DETEKT
85

96
id("com.github.ben-manes.versions") version Plugin.VERSIONS
107

11-
kotlin("jvm") version Plugin.KOTLIN
128
`java-library`
13-
14-
id("org.jetbrains.dokka") version Plugin.DOKKA
159
idea
1610

1711
signing
@@ -23,8 +17,8 @@ version = "3.0.0-SNAPSHOT"
2317

2418
tasks {
2519
create<Exec>("generateHeader") {
26-
dependsOn("compileKotlin")
27-
val classpath = "$buildDir/classes/kotlin/main"
20+
dependsOn("compileJava")
21+
val classpath = "$buildDir/classes/java/main"
2822
val output = "native/volctl.h"
2923
setCommandLine(
3024
"javah",
@@ -47,20 +41,9 @@ tasks {
4741
setCommandLine("cmake", "--build", "build", "--config", "release")
4842
}
4943

50-
"dokka"(DokkaTask::class) {
51-
outputFormat = "html"
52-
outputDirectory = "$buildDir/kdoc"
53-
}
54-
55-
@Suppress("UNUSED_VARIABLE")
56-
val dokkaJavadoc by creating(DokkaTask::class) {
57-
outputFormat = "javadoc"
58-
outputDirectory = "$buildDir/javadoc"
59-
}
60-
6144
@Suppress("UNUSED_VARIABLE")
6245
val javadocJar by creating(Jar::class) {
63-
dependsOn("dokkaJavadoc")
46+
dependsOn("javadoc")
6447
archiveClassifier.set("javadoc")
6548
from("$buildDir/javadoc")
6649
}
@@ -71,12 +54,6 @@ tasks {
7154
from(sourceSets["main"].allSource)
7255
}
7356

74-
withType<KotlinCompile> {
75-
kotlinOptions {
76-
jvmTarget = "1.8"
77-
}
78-
}
79-
8057
withType<Jar> {
8158
from(project.projectDir) {
8259
include("LICENSE")
@@ -102,8 +79,11 @@ sourceSets {
10279
}
10380

10481
spotless {
105-
kotlin {
106-
ktlint()
82+
java {
83+
indentWithSpaces(4)
84+
trimTrailingWhitespace()
85+
removeUnusedImports()
86+
encoding = Charsets.UTF_8
10787
lineEndings = LineEnding.UNIX
10888
endWithNewline()
10989
}
@@ -119,21 +99,18 @@ spotless {
11999
}
120100
}
121101

122-
detekt {
123-
toolVersion = Plugin.DETEKT
124-
config = files("$rootDir/buildConfig/detekt.yml")
125-
buildUponDefaultConfig = true
126-
}
127-
128102
idea {
129103
module {
130104
isDownloadJavadoc = true
131105
}
132106
}
133107

134108
dependencies {
135-
implementation(kotlin("stdlib"))
136-
109+
compileOnly(
110+
group = "org.jetbrains",
111+
name = "annotations",
112+
version = Lib.NULL_ANNOTATIONS
113+
)
137114
testImplementation(
138115
group = "org.junit.jupiter",
139116
name = "junit-jupiter-api",

buildSrc/src/main/kotlin/Lib.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
object Lib {
2-
const val KOTLIN = Plugin.KOTLIN
3-
2+
const val NULL_ANNOTATIONS = "16.0.2"
43
const val JUNIT = "5.5.2"
54
}

buildSrc/src/main/kotlin/Plugin.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
object Plugin {
22
const val SPOTLESS = "3.24.3"
3-
const val DETEKT = "1.0.1"
43

54
const val VERSIONS = "0.25.0"
6-
7-
const val KOTLIN = "1.3.50"
8-
const val DOKKA = "0.9.18"
95
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package net.bjoernpetersen.volctl;
2+
3+
import org.jetbrains.annotations.NotNull;
4+
5+
final class StringUtils {
6+
private StringUtils() {
7+
}
8+
9+
@NotNull
10+
static String substringBeforeLast(@NotNull String s, char chr) {
11+
int index = s.lastIndexOf(chr);
12+
if (index < 0) return s;
13+
return s.substring(0, index);
14+
}
15+
16+
@NotNull
17+
static String substringAfterLast(@NotNull String s, char chr) {
18+
int index = s.lastIndexOf(chr);
19+
if (index < 0) return s;
20+
return s.substring(index + 1);
21+
}
22+
}
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
package net.bjoernpetersen.volctl;
2+
3+
import org.jetbrains.annotations.NotNull;
4+
5+
import java.io.IOException;
6+
import java.nio.channels.Channels;
7+
import java.nio.channels.FileChannel;
8+
import java.nio.channels.FileLock;
9+
import java.nio.channels.ReadableByteChannel;
10+
import java.nio.file.Files;
11+
import java.nio.file.Path;
12+
import java.nio.file.Paths;
13+
import java.nio.file.StandardOpenOption;
14+
15+
import static net.bjoernpetersen.volctl.StringUtils.substringAfterLast;
16+
import static net.bjoernpetersen.volctl.StringUtils.substringBeforeLast;
17+
18+
/**
19+
* Allows master system audio volume access and control.
20+
* <p>
21+
* This implementation uses JNI to perform the required native system calls. To do that it has to
22+
* store an included library file outside of the .jar-file. The location and name of that file can
23+
* be customized using the constructor parameters.
24+
*
25+
* <h2>Usage with multiple class loaders</h2>
26+
* <p>
27+
* If you plan to use this class from multiple class loaders, you'll have to set
28+
* {@code supportMultipleClassLoaders} to {@code true}. If you do that,
29+
* <b>every instance will export its own library file</b>. These files won't have a fully
30+
* predictable filename and cannot be deleted by the same JVM that has loaded them.
31+
* This workaround is necessary because only one {@link ClassLoader} is allowed to load a
32+
* library file.
33+
* <p>
34+
* <b>Note:</b> You may use the {@link #newInstanceWithClassLoaderSupport} method if you
35+
* want to use the default location and name, but enable multiple class loader support.
36+
*/
37+
@SuppressWarnings({"unused", "WeakerAccess"})
38+
public class VolumeControl {
39+
/**
40+
* The minimum value the volume may have.
41+
*/
42+
public static final int MIN_VOLUME = 0;
43+
/**
44+
* The maximum value the volume may have.
45+
*/
46+
public static final int MAX_VOLUME = 100;
47+
48+
private static final String LIB_NAME = "volctl";
49+
private static final String TMP_DIR_PROPERTY_NAME = "java.io.tmpdir";
50+
51+
/**
52+
* Constructor.
53+
*
54+
* @param dllLocation the directory to store the native access library in,
55+
* defaults to temp dir
56+
* @param dllName the name of the library file without file extension,
57+
* defaults to "volctl"
58+
* @param supportMultipleClassLoaders whether to create library files with different names for
59+
* each new instance (see {@link VolumeControl class docs})
60+
* @throws IOException if the library file can't be exported or loaded
61+
*/
62+
public VolumeControl(
63+
@NotNull Path dllLocation,
64+
@NotNull String dllName,
65+
boolean supportMultipleClassLoaders
66+
) throws IOException {
67+
String defaultLibFile = getDefaultLibFileName();
68+
String extension = substringAfterLast(defaultLibFile, '.');
69+
final Path path;
70+
if (supportMultipleClassLoaders) {
71+
// Every instance gets its own library file
72+
path = Files.createTempFile(dllLocation, dllName, '.' + extension);
73+
FileChannel channel = FileChannel.open(path, StandardOpenOption.WRITE);
74+
writeLibraryFile(channel);
75+
} else {
76+
path = dllLocation.resolve(dllName + '.' + extension);
77+
boolean writeFile = true;
78+
if (Files.isRegularFile(path)) {
79+
try {
80+
// Try to delete outdated library file
81+
Files.delete(path);
82+
} catch (IOException e) {
83+
// Errors are most likely caused by another instance using the existing file,
84+
// so we can just ignore it and use the file
85+
writeFile = false;
86+
}
87+
}
88+
89+
if (writeFile) {
90+
FileChannel channel = FileChannel
91+
.open(path, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
92+
writeLibraryFile(channel);
93+
}
94+
}
95+
96+
System.load(path.toAbsolutePath().toString());
97+
}
98+
99+
/**
100+
* Same as calling
101+
* {@link #VolumeControl(Path, String, boolean) VolumeControl(dllLocation, dllName, false)}.
102+
*
103+
* @param dllLocation the directory to store the native access library in,
104+
* defaults to temp dir
105+
* @param dllName the name of the library file without file extension,
106+
* defaults to "volctl"
107+
* @throws IOException if the library file can't be exported or loaded
108+
*/
109+
public VolumeControl(@NotNull Path dllLocation, @NotNull String dllName) throws IOException {
110+
this(dllLocation, dllName, false);
111+
}
112+
113+
/**
114+
* Same as calling
115+
* {@link #VolumeControl(Path, String) VolumeControl(dllLocation, getDefaultLibName())}.
116+
*
117+
* @param dllLocation the directory to store the native access library in,
118+
* defaults to temp dir
119+
* @throws IOException if the library file can't be exported or loaded
120+
*/
121+
public VolumeControl(@NotNull Path dllLocation) throws IOException {
122+
this(dllLocation, getDefaultLibName());
123+
}
124+
125+
/**
126+
* Same as calling
127+
* {@link #VolumeControl(Path, String) VolumeControl(getTempDir(), dllName)}.
128+
*
129+
* @param dllName the name of the library file without file extension,
130+
* defaults to "volctl"
131+
* @throws IOException if the library file can't be exported or loaded
132+
*/
133+
public VolumeControl(@NotNull String dllName) throws IOException {
134+
this(getTempDir(), dllName);
135+
}
136+
137+
/**
138+
* Same as calling
139+
* {@link #VolumeControl(Path) VolumeControl(getTempDir())}.
140+
*
141+
* @throws IOException if the library file can't be exported or loaded
142+
*/
143+
public VolumeControl() throws IOException {
144+
this(getTempDir());
145+
}
146+
147+
/**
148+
* Gets the current master audio volume level.
149+
*
150+
* @return a value between 0 and 100 (inclusively)
151+
*/
152+
public int getVolume() {
153+
return getVolumeNative();
154+
}
155+
156+
/**
157+
* Sets the current master audio volume level.
158+
*
159+
* @param value a value between 0 and 100 (inclusively)
160+
*/
161+
public void setVolume(int value) {
162+
if (value < MIN_VOLUME)
163+
throw new IllegalStateException("Value must be positive, was " + value);
164+
if (value > MAX_VOLUME)
165+
throw new IllegalStateException("Value must be less than 100, was " + value);
166+
setVolumeNative(value);
167+
}
168+
169+
private native int getVolumeNative();
170+
171+
private native void setVolumeNative(int value);
172+
173+
/**
174+
* Creates a VolumeControl instance with defaults except for the
175+
* {@code supportMultipleClassLoaders} parameter being true.
176+
*
177+
* @return a new VolumeControl instance
178+
* @throws IOException if the library file couldn't be exported or loaded
179+
*/
180+
@NotNull
181+
public static VolumeControl newInstanceWithClassLoaderSupport() throws IOException {
182+
return new VolumeControl(getTempDir(), getDefaultLibName(), true);
183+
}
184+
185+
/**
186+
* Retrieves the directory for temporary files on the current system.
187+
*
188+
* @return the path to the tmp directory
189+
*/
190+
@NotNull
191+
public static Path getTempDir() {
192+
String path = System.getProperty(TMP_DIR_PROPERTY_NAME);
193+
return Paths.get(path);
194+
}
195+
196+
/**
197+
* @return the default library file name for the current OS, including extension
198+
*/
199+
@NotNull
200+
public static String getDefaultLibFileName() {
201+
return System.mapLibraryName(LIB_NAME);
202+
}
203+
204+
/**
205+
* @return the default library file name for the current OS, excluding extension
206+
*/
207+
@NotNull
208+
public static String getDefaultLibName() {
209+
return substringBeforeLast(getDefaultLibFileName(), '.');
210+
}
211+
212+
private static void writeLibraryFile(@NotNull FileChannel channel) throws IOException {
213+
try {
214+
FileLock lock = channel.lock();
215+
try (ReadableByteChannel input = Channels.newChannel(
216+
VolumeControl.class
217+
.getResourceAsStream('/' + getDefaultLibFileName()))) {
218+
channel.transferFrom(input, 0, Long.MAX_VALUE);
219+
} finally {
220+
lock.release();
221+
}
222+
} finally {
223+
channel.close();
224+
}
225+
}
226+
}

0 commit comments

Comments
 (0)