├── .github └── workflows │ └── gradle.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── multisample.iml ├── pom.xml ├── settings.gradle ├── src ├── main │ └── java │ │ └── com │ │ └── bitwig │ │ └── multisample │ │ ├── Group.java │ │ ├── Key.java │ │ ├── Loop.java │ │ ├── LoopMode.java │ │ ├── Multisample.java │ │ ├── Range.java │ │ ├── Sample.java │ │ ├── Util.java │ │ └── ZoneLogic.java └── test │ └── java │ └── com │ └── bitwig │ └── multisample │ └── MultisampleTest.java └── test-data └── Toy Xylophone.multisample /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Java CI with Gradle 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 12 20 | uses: actions/setup-java@v2 21 | with: 22 | java-version: '12' 23 | distribution: 'adopt' 24 | - name: Grant execute permission for gradlew 25 | run: chmod +x gradlew 26 | - name: Build with Gradle 27 | run: ./gradlew build 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.war 15 | *.nar 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | 21 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 22 | hs_err_pid* 23 | /build/ 24 | /target/ 25 | .idea 26 | .settings 27 | .project 28 | .classpath 29 | .gradle 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "automatic" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Bitwig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # .multisample 2 | 3 | A modern and open multi-sample file format for data exchange between sample-based instruments, to simplify the workflow for end users and sound designers. The format is limited to basic sample mappings, leaving out all playback and sound-shaping parameters to the preset of the relevant instrument. 4 | 5 | Authors: 6 | Bitwig & PreSonus Software 7 | 8 | ## Goals 9 | 10 | * As a sample-player user I would like to transfer a single, playable instrument sound that contains multiple single samples across a music keyboard, with multiple velocity layers and possibly multiple samples playing simultaneously. [in scope] 11 | 12 | * As a drum-sample-player user I would like to transfer a single drum or percussion instrument into an existing kit that contains multiple single samples in velocity layers, with multiple alternate samples (round-robin) per dynamic layer. [in scope] 13 | 14 | ## Non-goals 15 | 16 | * As a producer, I would like to transfer a set of corresponding drum or instrument loops with multiple musical variations. [out of scope] 17 | 18 | * As a sample-player user I would like to transfer a complex instrument containing multiple articulations and / or voicing (filters, envelopes, effects). [out of scope] 19 | 20 | ## Container Format 21 | 22 | A multisample can be packed into a single file using the ZIP container format. The mapping information is stored in the multisample.xml file in the root folder of the archive. 23 | 24 | Filename extension: .multisample 25 | 26 | Alternatively, a file system folder can be used, following the same naming convention. 27 | 28 | The ZIP file should embed files uncompressed using the STORED method to allow sample-data to be streamed without prior extraction. 29 | 30 | ## Audio Format 31 | 32 | Embedded sample files should be RIFF WAVE files in either PCM or IEEE_FLOAT format. A player is expected to play back mono and stereo files with arbitrary sample-rates and bit-depths between 8 and 32. 33 | 34 | Multichannel files are allowed, but it is not to be expected that all players support it. 35 | 36 | ## XML Structure 37 | 38 | A multisample may contain groups, which organize samples into logical groups. A group does not have any meaning for the sound synthesis, it is used to organize samples in an editor. 39 | 40 | A sample references an audio file on disk and adds mapping information. It can either be ungrouped (-1) or be part of a specified group. 41 | 42 | ```xml 43 | 44 | 45 | 46 | 47 | 48 | 49 | ``` 50 | 51 | | name | String attribute | Name of the multisample. | 52 | |-------------|------------------|-------------------------------------------------------| 53 | | generator | String element | Software which generated the file | 54 | | category* | String element | Category of the multisample (ie Drum / Keyboard / FX) | 55 | | creator | String element | User who created the file. | 56 | | description | String element | A longer description of the multisample. | 57 | | keywords | | | 58 | 59 | ## Multisample Definition 60 | 61 | Apart from the groups and samples, the multisample element also contain metadata of the sample. 62 | 63 | ```xml 64 | 65 | Bitwig Studio 66 | Bass 67 | Genys 68 | A nice acoustics bass recorded at a 9562 m altitude. 69 | 70 | Bass 71 | Acoustic 72 | Plucked 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | ``` 81 | 82 | | Property | Unit | Description | Default | 83 | |----------|------------|-----------------------------|---------| 84 | | sample | | | | 85 | | name | String | Displayed name of the group | | 86 | | color | Hex string | Like in HTML: d92e24 | | 87 | 88 | ## Sample definition 89 | 90 | ```xml 91 | 92 | 93 | 94 | 95 | 96 | ``` 97 | 98 | | Property | Unit | Description | Default | 99 | |--------------|---------------------------|----------------------------------------------------------|-------------| 100 | | sample | | | | 101 | | file | String | Relative path to audio file | | 102 | | sample-start | Samples | Sample start position | 0.0 | 103 | | sample-stop | Samples | Sample end position | file length | 104 | | gain | Decibels | | 0.0 | 105 | | group | int | Index of group (-1 means ungrouped) | -1 | 106 | | parameter-1 | float -1 … 1 | Exposed as P1 modulation source | 0 | 107 | | parameter-2 | float -1 … 1 | Exposed as P2 modulation source | 0 | 108 | | parameter-3 | float -1 … 1 | Exposed as P3 modulation source | 0 | 109 | | reverse | bool | | false | 110 | | zone-logic | String | always-play or round-robin | | 111 | | key | | | | 112 | | root | semitone | | 60 (C3) | 113 | | track | float 0 … 2 | 1 = 100% | 1 | 114 | | tune | relative semitone (float) | Fine tuning or transposition of the sample. | 0.0 | 115 | | low | semitone | | 0 | 116 | | high | semitone | | 127 | 117 | | low-fade | semitone | | 0 | 118 | | high-fade | semitone | | 0 | 119 | | velocity | | | | 120 | | low | velocity | | 1 | 121 | | high | velocity | | 127 | 122 | | low-fade | velocity | | 0 | 123 | | high-fade | velocity | | 0 | 124 | | select | | | | 125 | | low | select | | 1 | 126 | | high | select | | 127 | 127 | | low-fade | select | | 0 | 128 | | high-fade | select | | 0 | 129 | | loop | | | | 130 | | mode | String | off, loop or ping-pong | off | 131 | | start | Samples, float | | 0.0 | 132 | | stop | Samples, float | | file length | 133 | | fade | 0 .. 1 | Multiply with (stop - start) to the get crossfade length | 0.0 | 134 | 135 | ## XML Schema 136 | 137 | ```xml 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | ``` 234 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | */ 4 | 5 | plugins { 6 | id 'java' 7 | id 'maven-publish' 8 | } 9 | 10 | repositories { 11 | mavenLocal() 12 | maven { 13 | url = uri('https://public-snapshots') 14 | } 15 | 16 | maven { 17 | url = uri('https://public-releases') 18 | } 19 | 20 | maven { 21 | url = uri('https://repo.maven.apache.org/maven2/') 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation 'jakarta.xml.bind:jakarta.xml.bind-api:3.0.0' 27 | implementation 'com.sun.xml.bind:jaxb-impl:3.0.0' 28 | implementation 'javax.activation:activation:1.1.1' 29 | testImplementation 'junit:junit:4.12' 30 | } 31 | 32 | group = 'com.bitwig' 33 | version = '1' 34 | description = '.multisample file format' 35 | java.sourceCompatibility = JavaVersion.VERSION_12 36 | 37 | publishing { 38 | publications { 39 | maven(MavenPublication) { 40 | from(components.java) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwig/multisample/4e7971f18848114f229bdefcb2bb47552f55aff6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /multisample.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | com.bitwig 7 | multisample 8 | jar 9 | .multisample file format 10 | 1 11 | 12 | 13 | 14 | central 15 | https://repo.maven.apache.org/maven2/ 16 | 17 | 18 | 19 | 20 | 21 | junit 22 | junit 23 | 4.13.1 24 | test 25 | 26 | 27 | jakarta.xml.bind 28 | jakarta.xml.bind-api 29 | 3.0.0 30 | 31 | 32 | com.sun.xml.bind 33 | jaxb-impl 34 | 3.0.0 35 | 36 | 37 | javax.activation 38 | activation 39 | 1.1.1 40 | 41 | 42 | 43 | 44 | 45 | 46 | org.apache.maven.plugins 47 | maven-compiler-plugin 48 | 3.5.1 49 | 50 | true 51 | true 52 | 12 53 | 12 54 | UTF-8 55 | 1024m 56 | 57 | 58 | 59 | org.codehaus.mojo 60 | jaxb2-maven-plugin 61 | 62 | 63 | schemagen 64 | 65 | schemagen 66 | 67 | 68 | 69 | 70 | 71 | \.Util.java 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | */ 4 | 5 | rootProject.name = 'multisample' 6 | -------------------------------------------------------------------------------- /src/main/java/com/bitwig/multisample/Group.java: -------------------------------------------------------------------------------- 1 | package com.bitwig.multisample; 2 | 3 | import jakarta.xml.bind.annotation.XmlAttribute; 4 | import jakarta.xml.bind.annotation.XmlRootElement; 5 | 6 | @XmlRootElement 7 | public class Group 8 | { 9 | @XmlAttribute(required = true) 10 | public String name; 11 | 12 | @XmlAttribute 13 | public String color; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/bitwig/multisample/Key.java: -------------------------------------------------------------------------------- 1 | package com.bitwig.multisample; 2 | 3 | import jakarta.xml.bind.annotation.XmlAttribute; 4 | 5 | public class Key extends Range 6 | { 7 | @XmlAttribute 8 | Integer root; 9 | 10 | /** Key-tracking range = [0 .. 2] default = 1 (100%) */ 11 | @XmlAttribute 12 | Double track; 13 | 14 | /** Fine tuning or transposition of the sample, in (fractional) semitones. (default = 0.0) */ 15 | @XmlAttribute 16 | Double tune; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/bitwig/multisample/Loop.java: -------------------------------------------------------------------------------- 1 | package com.bitwig.multisample; 2 | 3 | import jakarta.xml.bind.annotation.XmlAttribute; 4 | 5 | public class Loop 6 | { 7 | /** 8 | * Loop mode - off/loop/ping-pong 9 | */ 10 | @XmlAttribute 11 | public LoopMode mode; 12 | 13 | /** 14 | * Loop start in samples. 15 | * Fractional positions are allowed and can be used for loop tuning. 16 | * */ 17 | @XmlAttribute 18 | public Double start; 19 | 20 | /** 21 | * Loop stop (end) in samples. 22 | * Fractional positions are allowed and can be used for loop tuning. 23 | * */ 24 | @XmlAttribute 25 | public Double stop; 26 | 27 | /** Fade length [0 .. 1] 28 | Multiply with (stop - start) to the get crossfade length in samples 29 | */ 30 | @XmlAttribute 31 | public Double fade; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/bitwig/multisample/LoopMode.java: -------------------------------------------------------------------------------- 1 | package com.bitwig.multisample; 2 | 3 | import jakarta.xml.bind.annotation.XmlEnum; 4 | import jakarta.xml.bind.annotation.XmlEnumValue; 5 | 6 | @XmlEnum 7 | public enum LoopMode 8 | { 9 | @XmlEnumValue(value = "off") off, 10 | @XmlEnumValue(value = "loop") loop, 11 | @XmlEnumValue(value = "ping-pong") pingPong 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/bitwig/multisample/Multisample.java: -------------------------------------------------------------------------------- 1 | package com.bitwig.multisample; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import jakarta.xml.bind.annotation.*; 7 | 8 | @XmlRootElement(name = "multisample") 9 | @XmlSeeAlso({Group.class, Sample.class}) 10 | public class Multisample 11 | { 12 | /** 13 | * Name of the multisample. 14 | */ 15 | @XmlAttribute(required = true) 16 | public String name; 17 | 18 | /** 19 | * Software which generated the file. 20 | */ 21 | @XmlElement(required = true) 22 | public String generator; 23 | 24 | /** Category of the multisample (ie Drum / Keyboard / FX) */ 25 | @XmlElement(required = true) 26 | public String category; 27 | 28 | /** User who created the file. */ 29 | @XmlElement(required = true) 30 | public String creator; 31 | 32 | /** A longer description of the multisample. */ 33 | @XmlElement(required = false) 34 | public String description; 35 | 36 | @XmlElementWrapper(name = "keywords") 37 | @XmlElement(name = "keyword") 38 | public List keywords = new ArrayList<>(); 39 | 40 | @XmlElementRef 41 | public List groups = new ArrayList<>(); 42 | 43 | @XmlElementRef 44 | public List samples = new ArrayList<>(); 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/bitwig/multisample/Range.java: -------------------------------------------------------------------------------- 1 | package com.bitwig.multisample; 2 | 3 | import jakarta.xml.bind.annotation.XmlAttribute; 4 | 5 | public class Range 6 | { 7 | @XmlAttribute Integer low; 8 | @XmlAttribute Integer high; 9 | 10 | @XmlAttribute(name = "low-fade") Integer lowFade; 11 | @XmlAttribute(name = "high-fade") Integer highFade; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/bitwig/multisample/Sample.java: -------------------------------------------------------------------------------- 1 | package com.bitwig.multisample; 2 | 3 | import jakarta.xml.bind.annotation.XmlAttribute; 4 | import jakarta.xml.bind.annotation.XmlElement; 5 | import jakarta.xml.bind.annotation.XmlRootElement; 6 | 7 | @XmlRootElement 8 | public class Sample 9 | { 10 | /** Relative path to audio file. */ 11 | @XmlAttribute(required = true) 12 | public String file; 13 | 14 | /** Sample start position in samples (default = 0.0). 15 | * Fractional positions are allowed 16 | * */ 17 | @XmlAttribute(name = "sample-start") 18 | public Double sampleStart; 19 | 20 | /** Sample end position (default = file length in samples). 21 | * Fractional positions are allowed 22 | * */ 23 | @XmlAttribute(name = "sample-stop") 24 | public Double sampleStop; 25 | 26 | /** Gain correction in decibels (default = 0.0) */ 27 | @XmlAttribute 28 | public Double gain; 29 | 30 | /** Index of group (default = -1 = ungrouped) */ 31 | @XmlAttribute 32 | public Integer group; 33 | 34 | /** Exposed as P1 modulation source */ 35 | @XmlAttribute(name = "parameter-1") 36 | public Double parameter1; 37 | 38 | /** Exposed as P2 modulation source */ 39 | @XmlAttribute(name = "parameter-2") 40 | public Double parameter2; 41 | 42 | /** Exposed as P3 modulation source */ 43 | @XmlAttribute(name = "parameter-3") 44 | public Double parameter3; 45 | 46 | /** Reverse playback when true */ 47 | @XmlAttribute 48 | public Boolean reverse; 49 | 50 | /** Zone logic, always-play or round-robin */ 51 | @XmlAttribute(name = "zone-logic") 52 | public ZoneLogic zoneLogic = ZoneLogic.alwaysPlay; 53 | 54 | @XmlElement(required = true) 55 | public Key key = new Key(); 56 | 57 | @XmlElement(required = true) 58 | public Range velocity = new Range(); 59 | 60 | @XmlElement(required = true) 61 | public Range select = new Range(); 62 | 63 | @XmlElement(required = false) 64 | public Loop loop = new Loop(); 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/bitwig/multisample/Util.java: -------------------------------------------------------------------------------- 1 | package com.bitwig.multisample; 2 | 3 | import javax.xml.XMLConstants; 4 | import javax.xml.transform.Result; 5 | import javax.xml.transform.stream.StreamResult; 6 | import javax.xml.validation.Schema; 7 | import javax.xml.validation.SchemaFactory; 8 | import java.io.File; 9 | import java.io.FileInputStream; 10 | import java.io.FileOutputStream; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.io.InputStreamReader; 14 | import java.io.StringReader; 15 | import java.io.StringWriter; 16 | import java.nio.charset.StandardCharsets; 17 | import java.util.Map; 18 | import java.util.zip.CRC32; 19 | import java.util.zip.ZipEntry; 20 | import java.util.zip.ZipFile; 21 | import java.util.zip.ZipOutputStream; 22 | 23 | import jakarta.xml.bind.JAXBContext; 24 | import jakarta.xml.bind.JAXBException; 25 | import jakarta.xml.bind.Marshaller; 26 | import jakarta.xml.bind.SchemaOutputResolver; 27 | import org.xml.sax.SAXException; 28 | 29 | public class Util 30 | { 31 | public static final String FORMAT_NAME = "DAW-project exchange format"; 32 | public static final String FILE_EXTENSION = "multisample"; 33 | 34 | private static final String PROJECT_FILE = "multisample.xml"; 35 | 36 | public static void exportSchema(File file, Class cls) throws IOException 37 | { 38 | try 39 | { 40 | var context = createContext(cls); 41 | 42 | var resolver = new SchemaOutputResolver() 43 | { 44 | @Override public Result createOutput (String namespaceUri, String suggestedFileName) throws IOException 45 | { 46 | FileOutputStream fileOutputStream = new FileOutputStream(file); 47 | StreamResult result = new StreamResult(fileOutputStream); 48 | result.setSystemId(file.getName()); 49 | return result; 50 | } 51 | }; 52 | 53 | context.generateSchema(resolver); 54 | } 55 | catch (JAXBException e) 56 | { 57 | throw new IOException(e); 58 | } 59 | } 60 | 61 | private static String toXML(Object object) throws IOException 62 | { 63 | try 64 | { 65 | var context = createContext(object.getClass()); 66 | 67 | var marshaller = context.createMarshaller(); 68 | marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); 69 | 70 | var sw = new StringWriter(); 71 | marshaller.marshal(object, sw); 72 | 73 | return sw.toString(); 74 | } 75 | catch (Exception e) 76 | { 77 | throw new IOException(e); 78 | } 79 | } 80 | 81 | private static JAXBContext createContext(final Class cls) throws JAXBException 82 | { 83 | return JAXBContext.newInstance(cls); 84 | } 85 | 86 | private static T fromXML(InputStreamReader reader, Class cls) throws IOException 87 | { 88 | try 89 | { 90 | var jaxbContext = JAXBContext.newInstance(cls); 91 | 92 | final var unmarshaller = jaxbContext.createUnmarshaller(); 93 | 94 | final var object = (T)unmarshaller.unmarshal(reader); 95 | 96 | return object; 97 | } 98 | catch (JAXBException e) 99 | { 100 | throw new IOException(e); 101 | } 102 | } 103 | 104 | public static void saveXML(Multisample multisample, File file) throws IOException 105 | { 106 | String projectXML = toXML(multisample); 107 | FileOutputStream fileOutputStream = new FileOutputStream(file); 108 | fileOutputStream.write(projectXML.getBytes(StandardCharsets.UTF_8)); 109 | fileOutputStream.close(); 110 | } 111 | 112 | public static void validate(Multisample multisample) throws IOException 113 | { 114 | String projectXML = toXML(multisample); 115 | 116 | try 117 | { 118 | var context = createContext(Multisample.class); 119 | 120 | final var schemaFile = File.createTempFile("schema", ".xml"); 121 | exportSchema(schemaFile, Multisample.class); 122 | 123 | SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); 124 | Schema schema = sf.newSchema(schemaFile); 125 | 126 | final var unmarshaller = context.createUnmarshaller(); 127 | unmarshaller.setSchema(schema); 128 | 129 | unmarshaller.unmarshal(new StringReader(projectXML)); 130 | } 131 | catch (JAXBException e) 132 | { 133 | throw new IOException(e); 134 | } 135 | catch (SAXException e) 136 | { 137 | throw new IOException(e); 138 | } 139 | } 140 | 141 | public static void save(Multisample multisample, Map embeddedFiles, File file) throws IOException 142 | { 143 | String projectXML = toXML(multisample); 144 | 145 | final ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(file)); 146 | zos.setMethod(ZipOutputStream.STORED); 147 | 148 | addUncompressedFileToZip(zos, PROJECT_FILE, projectXML.getBytes(StandardCharsets.UTF_8)); 149 | 150 | for (Map.Entry entry : embeddedFiles.entrySet()) 151 | { 152 | addUncompressedFileToZip(zos, entry.getValue(), entry.getKey()); 153 | } 154 | 155 | zos.close(); 156 | } 157 | 158 | private static void addUncompressedFileToZip( 159 | final ZipOutputStream zos, 160 | final String pathInZip, 161 | final byte[] data) throws IOException 162 | { 163 | final ZipEntry entry = new ZipEntry(pathInZip); 164 | 165 | final CRC32 crc = new CRC32(); 166 | crc.update(data); 167 | 168 | entry.setSize(data.length); 169 | entry.setCompressedSize(data.length); 170 | entry.setCrc(crc.getValue()); 171 | entry.setMethod(ZipOutputStream.STORED); 172 | zos.putNextEntry(entry); 173 | zos.write(data); 174 | zos.closeEntry(); 175 | } 176 | 177 | public static void addUncompressedFileToZip( 178 | final ZipOutputStream zos, 179 | final String pathInZip, 180 | final File file) throws IOException 181 | { 182 | final ZipEntry entry = new ZipEntry(pathInZip); 183 | 184 | final CRC32 crc = new CRC32(); 185 | 186 | final byte[] buffer = new byte[1024]; 187 | 188 | final InputStream input = new FileInputStream(file); 189 | long length = 0; 190 | 191 | try 192 | { 193 | while (true) 194 | { 195 | final int amountRead = input.read(buffer); 196 | 197 | assert amountRead > 0 || amountRead == -1; 198 | 199 | if (amountRead > 0) 200 | { 201 | crc.update(buffer, 0, amountRead); 202 | length += amountRead; 203 | } 204 | 205 | if (amountRead == -1) 206 | { 207 | break; 208 | } 209 | } 210 | } 211 | finally 212 | { 213 | try 214 | { 215 | input.close(); 216 | } 217 | catch (final IOException e) 218 | { 219 | } 220 | } 221 | 222 | entry.setSize(length); 223 | entry.setCompressedSize(length); 224 | entry.setCrc(crc.getValue()); 225 | entry.setMethod(ZipOutputStream.STORED); 226 | zos.putNextEntry(entry); 227 | 228 | final InputStream input2 = new FileInputStream(file); 229 | 230 | try 231 | { 232 | long lengthWritten = 0; 233 | 234 | while (true) 235 | { 236 | final int amountRead = input2.read(buffer); 237 | 238 | if (amountRead == -1) 239 | { 240 | break; 241 | } 242 | 243 | assert amountRead > 0; 244 | 245 | lengthWritten += amountRead; 246 | 247 | if (lengthWritten > length) 248 | { 249 | throw new IOException("File size changed while writing"); 250 | } 251 | 252 | zos.write(buffer, 0, amountRead); 253 | } 254 | 255 | if (lengthWritten != length) 256 | { 257 | throw new IOException("File size changed while writing"); 258 | } 259 | 260 | zos.closeEntry(); 261 | } 262 | finally 263 | { 264 | try 265 | { 266 | input2.close(); 267 | } 268 | catch (final IOException e) 269 | { 270 | } 271 | } 272 | } 273 | 274 | public static Multisample loadMultisample(final File file) throws IOException 275 | { 276 | ZipFile zipFile = new ZipFile(file); 277 | 278 | ZipEntry projectEntry = zipFile.getEntry(PROJECT_FILE); 279 | 280 | Multisample multisample = fromXML(new InputStreamReader(zipFile.getInputStream(projectEntry)), Multisample.class); 281 | 282 | zipFile.close(); 283 | 284 | return multisample; 285 | } 286 | 287 | 288 | public static InputStream streamEmbedded(final File file, final String embeddedPath) throws IOException 289 | { 290 | ZipFile zipFile = new ZipFile(file); 291 | 292 | ZipEntry entry = zipFile.getEntry(embeddedPath); 293 | 294 | InputStream inputStream = zipFile.getInputStream(entry); 295 | 296 | return inputStream; 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/main/java/com/bitwig/multisample/ZoneLogic.java: -------------------------------------------------------------------------------- 1 | package com.bitwig.multisample; 2 | 3 | import jakarta.xml.bind.annotation.XmlEnum; 4 | import jakarta.xml.bind.annotation.XmlEnumValue; 5 | 6 | @XmlEnum 7 | public enum ZoneLogic 8 | { 9 | @XmlEnumValue("always-play") alwaysPlay, 10 | @XmlEnumValue("round-robin") roundRobin, 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/com/bitwig/multisample/MultisampleTest.java: -------------------------------------------------------------------------------- 1 | package com.bitwig.multisample; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.util.HashMap; 10 | 11 | public class MultisampleTest 12 | { 13 | private Multisample createDummyMultisample() 14 | { 15 | final var multisample = new Multisample(); 16 | 17 | multisample.name = "Dummy"; 18 | multisample.generator = "MultisampleTest"; 19 | multisample.category = "Winds"; 20 | multisample.creator = "ACME"; 21 | multisample.description = "Test multisample generated by " + multisample.generator; 22 | multisample.keywords.add("test"); 23 | multisample.keywords.add("dummy"); 24 | 25 | return multisample; 26 | } 27 | 28 | @Test 29 | public void saveMultisample() throws IOException 30 | { 31 | var multisample = createDummyMultisample(); 32 | 33 | new File("target").mkdirs(); 34 | Util.save(multisample, new HashMap(), new File("target/test.multisample")); 35 | Util.saveXML(multisample, new File("target/test.multisample.xml")); 36 | } 37 | 38 | @Test 39 | public void validateMultisample() throws IOException 40 | { 41 | var multisample = createDummyMultisample(); 42 | Util.validate(multisample); 43 | } 44 | 45 | @Test 46 | public void saveAndLoadMultisample() throws IOException 47 | { 48 | var multisample = createDummyMultisample(); 49 | 50 | final var file = File.createTempFile("testfile", ".multisample"); 51 | Util.save(multisample, new HashMap(), file); 52 | 53 | final var loadedMultisample = Util.loadMultisample(file); 54 | 55 | Assert.assertEquals(multisample.groups.size(), loadedMultisample.groups.size()); 56 | Assert.assertEquals(multisample.samples.size(), loadedMultisample.samples.size()); 57 | } 58 | 59 | @Test 60 | public void writeSchema() throws IOException 61 | { 62 | new File("target").mkdirs(); 63 | Util.exportSchema(new File("target/multisample.xs"), Multisample.class); 64 | } 65 | 66 | @Test 67 | public void loadBitwigMultisample() throws IOException 68 | { 69 | File file = new File("test-data/Toy Xylophone.multisample"); 70 | Assert.assertTrue(file.exists()); 71 | Assert.assertTrue(file.isFile()); 72 | 73 | // try reading multisample first 74 | var project = Util.loadMultisample(file); 75 | Assert.assertNotNull(project); 76 | 77 | Assert.assertEquals(9, project.samples.size()); 78 | 79 | for (Sample sample : project.samples) 80 | { 81 | InputStream inputStream = Util.streamEmbedded(file, sample.file); 82 | byte[] data = inputStream.readAllBytes(); 83 | 84 | Assert.assertTrue(data.length > 10); 85 | } 86 | 87 | } 88 | 89 | @Test 90 | public void loadEmbeddedFile() throws IOException 91 | { 92 | File file = new File("test-data/Toy Xylophone.multisample"); 93 | Assert.assertTrue(file.exists()); 94 | Assert.assertTrue(file.isFile()); 95 | 96 | // try reading multisample first 97 | var project = Util.loadMultisample(file); 98 | Assert.assertNotNull(project); 99 | 100 | InputStream inputStream = Util.streamEmbedded(file, "Toy Xylo D2.wav"); 101 | 102 | byte[] data = inputStream.readAllBytes(); 103 | 104 | Assert.assertEquals(65660, data.length); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /test-data/Toy Xylophone.multisample: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwig/multisample/4e7971f18848114f229bdefcb2bb47552f55aff6/test-data/Toy Xylophone.multisample --------------------------------------------------------------------------------