├── awesome-melody ├── double-tigers.midercode ├── if-at-that-time-part-xusong.midercode ├── canon-part.midercode ├── dou-dizhu.midercode ├── grandma's-penghu-bay-part.midercode ├── surprise-part-jiangwen.midercode ├── mornig-in-school.midercode ├── jingle-bells-full.midercode ├── スパークル火花 动画电影《你的名字。》插曲.midercode ├── happy-birthday.midercode ├── do you hear the people sing.midercode ├── 0shoulldbe.nmc ├── listen-one ok rock.midercode ├── 偏偏喜欢你.midercode ├── Our Stories BV1cG4y1J7bZ.midercode └── Unwelcome School BV16V4y1A7xD.midercode ├── settings.gradle.kts ├── bot ├── src │ ├── main │ │ ├── resources │ │ │ ├── META-INF │ │ │ │ └── services │ │ │ │ │ └── net.mamoe.mirai.console.plugin.jvm.JvmPlugin │ │ │ ├── 2000-years-later.png │ │ │ └── melody-list.txt │ │ └── kotlin │ │ │ └── org │ │ │ └── mider │ │ │ └── produce │ │ │ └── bot │ │ │ ├── game │ │ │ └── logic.kt │ │ │ ├── utils │ │ │ └── MessasgeUtils.kt │ │ │ ├── handle.kt │ │ │ ├── BotConfiguration.kt │ │ │ └── MiderBot.kt │ └── test │ │ └── kotlin │ │ └── test.kt └── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── service ├── src │ ├── test │ │ ├── resources │ │ │ ├── test get resource.http │ │ │ ├── test-api.http │ │ │ └── test direct api.http │ │ └── kotlin │ │ │ └── org │ │ │ └── mider │ │ │ └── produce │ │ │ ├── service │ │ │ └── utlis │ │ │ │ └── StreamUtilsKtTest.kt │ │ │ └── ApplicationTest.kt │ ├── main │ │ ├── resources │ │ │ ├── application.conf │ │ │ ├── logback.xml │ │ │ ├── application.conf.default │ │ │ └── static │ │ │ │ └── index.html │ │ └── kotlin │ │ │ └── org │ │ │ └── mider │ │ │ └── produce │ │ │ └── service │ │ │ ├── logger │ │ │ └── AppLogger.kt │ │ │ ├── utlis │ │ │ ├── StreamUtils.kt │ │ │ └── Utils.kt │ │ │ ├── data │ │ │ ├── ResponseBody.kt │ │ │ └── ServiceParameter.kt │ │ │ ├── Application.kt │ │ │ └── plugins │ │ │ └── Routing.kt │ └── Dockerfile └── build.gradle.kts ├── core ├── src │ ├── main │ │ └── kotlin │ │ │ └── org │ │ │ └── mider │ │ │ └── produce │ │ │ └── core │ │ │ ├── utils │ │ │ ├── Utils.kt │ │ │ ├── TimeUtils.kt │ │ │ ├── FileUtils.kt │ │ │ ├── StringUtils.kt │ │ │ ├── ConverUtils.kt │ │ │ └── StreamUtils.kt │ │ │ ├── init.kt │ │ │ ├── logic.kt │ │ │ ├── Configuration.kt │ │ │ └── sinsy.kt │ └── test │ │ └── kotlin │ │ └── org │ │ └── mider │ │ └── produce │ │ └── test │ │ └── TestLogic.kt └── build.gradle.kts ├── .github └── workflows │ ├── DependencySubmission.yml │ ├── detekt.yml │ └── BuildServiceDocker.yml ├── cl ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── sinsy.kt │ ├── cl.kt │ ├── utils.kt │ └── MiderCodeCommandLine.kt ├── .devcontainer └── devcontainer.json ├── .gitignore ├── gradlew.bat ├── gradlew └── README.md /awesome-melody/double-tigers.midercode: -------------------------------------------------------------------------------- 1 | >g;2x>1231 1231 3450 3450 5-6-5-4-31 5-6-5-4-31 15!10 15!10 -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | rootProject.name = "MiderProduce" 3 | include("core", "bot", "service", "cl") -------------------------------------------------------------------------------- /bot/src/main/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin: -------------------------------------------------------------------------------- 1 | org.mider.produce.bot.MiderBot -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ktor_version=2.1.2 2 | kotlin_version=1.6.21 3 | logback_version=1.4.4 4 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiterasbk/MiraiMidiProduce/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /awesome-melody/if-at-that-time-part-xusong.midercode: -------------------------------------------------------------------------------- 1 | >g;2x>G+EmAvwvv^-vamc++:g am^^ G+wDvaD--^D-D++ G+EmAvwvv^-vamc+.:g g^m^^G+E~vvwD-vC++ -------------------------------------------------------------------------------- /service/src/test/resources/test get resource.http: -------------------------------------------------------------------------------- 1 | GET localhost:8080/generated/5839550cb2eb720202568ab1a43ae86120e9498d32c62b2752cef4db6e66cabc-1.png -------------------------------------------------------------------------------- /awesome-melody/canon-part.midercode: -------------------------------------------------------------------------------- 1 | >g;2x>A#F-G-A#F-G-A-a-^#C-D-^#F-G- #FD-E-#F #f-g-^^vv^#f-g-^ gb-vg#f-e-#f-e-d-e-#f-g-^^ gb-v b#C-D- a-^#C-D-^#F-G-^ -------------------------------------------------------------------------------- /bot/src/main/resources/2000-years-later.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiterasbk/MiraiMidiProduce/HEAD/bot/src/main/resources/2000-years-later.png -------------------------------------------------------------------------------- /service/src/test/resources/test-api.http: -------------------------------------------------------------------------------- 1 | POST localhost:8080/api 2 | Content-Type: application/json 3 | 4 | { 5 | "midercode": ">g>1155665 4433221" 6 | } -------------------------------------------------------------------------------- /awesome-melody/dou-dizhu.midercode: -------------------------------------------------------------------------------- 1 | >g;2x>E+EDC+Ca DEDEg++ a+aga+C+ GAEGD++E+EDE+G+ AAAC↑A+GE D+DEG+g+ DEDEC++ E+EDE+G+ AC↑AGA+GE D+DEG+g+DEDEC++DDDEG+GA C↑+A+C↑++ -------------------------------------------------------------------------------- /service/src/test/resources/test direct api.http: -------------------------------------------------------------------------------- 1 | POST localhost:8080/direct-api 2 | Content-Type: application/json 3 | 4 | { 5 | "midercode": ">g;4x;img>123342" 6 | } -------------------------------------------------------------------------------- /service/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | service { 2 | deployment { 3 | port = 5827 4 | } 5 | 6 | produce { 7 | debug = true 8 | } 9 | } -------------------------------------------------------------------------------- /awesome-melody/grandma's-penghu-bay-part.midercode: -------------------------------------------------------------------------------- 1 | >g;2x;Bmin;i=musicbox>faaabDba | b-D-~~Db-a-a++ | F~~~ GFED | E-~~~EE-F-E++ | FFF-F F-GFED | bDD b-a- a++ | FFF-F F-GFED | a-~~~EC D++ -------------------------------------------------------------------------------- /awesome-melody/surprise-part-jiangwen.midercode: -------------------------------------------------------------------------------- 1 | >g;2x>d+a+~~-.$b -a+...-.a$b CD+.DC+~ a+++d+g+~g.a-g++ ^ C.a-C++e.f-d+++ f+:a:D f+:a:F a+:E:A D.:$B:D↑ C-:A:C↑ D++.:$B:D↑ D.:$B:D↑ C-:A:C↑ D++:$B:D↑ C.:G:C↑ C-:G:C↑ a+++:F:A -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /service/src/main/kotlin/org/mider/produce/service/logger/AppLogger.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.service.logger 2 | 3 | import org.slf4j.Logger 4 | import org.slf4j.LoggerFactory 5 | 6 | object AppLogger { 7 | val logger: Logger = LoggerFactory.getLogger(this::class.java) 8 | } -------------------------------------------------------------------------------- /bot/build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | apply(plugin = "net.mamoe.mirai-console") 3 | dependencies { 4 | implementation(project(":core")) 5 | 6 | "shadowLink"("com.github.nwaldispuehl:java-lame") 7 | "shadowLink"("com.github.whiterasbk:mider") 8 | "shadowLink"("io.github.mzdluo123:silk4j") 9 | } -------------------------------------------------------------------------------- /awesome-melody/mornig-in-school.midercode: -------------------------------------------------------------------------------- 1 | >g;2x>do-b-ba-g-g-b↓-dd+ | eo-C-ba b-e-aa+ | o-b-bCD-E--D--b | o-b-b-a-g+ | o-a-a-b-a-g-#f | e-d-gg+ | b- b--b-- a-b- b #f-. #f-- | a- g. | a-a-a-b-a g-. e-- | b-a. | o-d-d-d-e#f-.#f-- | g-#fd-e+ | o-ee-#fga-b-b-g-a+ | o-b-b-C- D-D-.D-- | b- ag-e+ | o-EE-DbDo-d-e#f-.a-- g++ 2 | -------------------------------------------------------------------------------- /service/src/main/kotlin/org/mider/produce/service/utlis/StreamUtils.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.service.utlis 2 | 3 | import java.security.MessageDigest 4 | 5 | fun String.hash(): String = MessageDigest 6 | .getInstance("SHA-256") 7 | .digest(toByteArray()) 8 | .fold("") { str, it -> str + "%02x".format(it) } -------------------------------------------------------------------------------- /service/src/test/kotlin/org/mider/produce/service/utlis/StreamUtilsKtTest.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.service.utlis 2 | 3 | import org.junit.jupiter.api.Test 4 | 5 | internal class StreamUtilsKtTest { 6 | 7 | @Test 8 | fun hash() { 9 | val hash = "123 ".hash() 10 | println(hash) 11 | } 12 | } -------------------------------------------------------------------------------- /awesome-melody/jingle-bells-full.midercode: -------------------------------------------------------------------------------- 1 | >g;2x>gEDCg+ G↑--wm++ gEDCa+ G↑--wm++ aFEDb+ G↑--wm++ GGFDE+ F↑--vvv gEDCg+ G↑--w^^ gEDCa+ F↑--w^^ aFED G:b G:b G+:b A:D G:D F:d D:b C+:a A↑--vvw E:C E:C E+:C E:C E:C E+:C EGC.D--E+ + F:a F:a F+:a F:a E:g E+:g EDDCD+G+ E:C E:C E+:C E:C E:C E+:C EGC.D--E+ G↑--w^^ F:a F:a F+:a F:a E:g E+:g GGFDC+ -------------------------------------------------------------------------------- /core/src/main/kotlin/org/mider/produce/core/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.core.utils 2 | 3 | import org.mider.produce.core.Configuration 4 | 5 | fun Configuration.ifDebug(block: ()-> Unit) { 6 | if (debug) block() 7 | } 8 | 9 | fun Configuration.ifDebug(info: String) { 10 | if (debug) logger(info) 11 | } 12 | -------------------------------------------------------------------------------- /service/src/main/kotlin/org/mider/produce/service/data/ResponseBody.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.service.data 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ResponseBody( 7 | val stateCode: Int, 8 | val state: String, 9 | val message: String, 10 | val type: String? = null, 11 | val link: List>? = null 12 | ) 13 | -------------------------------------------------------------------------------- /service/src/test/kotlin/org/mider/produce/ApplicationTest.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce 2 | 3 | import kotlinx.serialization.decodeFromString 4 | import kotlinx.serialization.json.Json 5 | import org.junit.Test 6 | import org.mider.produce.service.data.ServiceParameter 7 | 8 | class ApplicationTest { 9 | 10 | @Test 11 | fun `test data service parameter`() { 12 | val b = Json.decodeFromString("""{}""") 13 | println(b) 14 | } 15 | } -------------------------------------------------------------------------------- /service/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /awesome-melody/スパークル火花 动画电影《你的名字。》插曲.midercode: -------------------------------------------------------------------------------- 1 | >160b;2x;4>(repeat 16:1↑2↑5↑1↑2↑5↑1↑2↑5↑1↑2↑5↑)(repeat 4:671↑671↑45645651↑3↑51↑3↑572↑572↑) 2 | >160b;2x;3>(repeat 4:3++0++0++4++0++0++5++0++0++6++005++00)(repeat 4:6006004004001↑001↑00500500) 3 | >160b;2x;6>0++033022011000000++000+00333022013003052001000++033042011000000++000+02222032011++000++000++033022011000000++000+00333022013003052001000++033042011000000++000+02222032011+0000++0055555055043021001023022005555055043023000212000001023040032011031020000001023040034050043022032011 4 | -------------------------------------------------------------------------------- /awesome-melody/happy-birthday.midercode: -------------------------------------------------------------------------------- 1 | >120b;i=recorder;128%>b#G E.-#F-- #GE|#Fbb o-.#g--|E#C.-#D--E#C|#DBBo|AA-.#G-- #F #G-.A--|#G#FE#C|EE-.#C--E#G|b.:E:#F #D. b|#G E-.#F-- #G E-E-|#F bb o-. #g-- E #C-.#D-- E #C-.#C-- #DBBo|AA-.#G--#F#G-.A--|#G#FE#C EE-#C-E#G #F.E- E#F #g++:b:E↟ 2 | >g;i=musicbox;128%>o e #g:b e #g:b b3 #d:#f #d:#f o|#c e:#g #c e:#g #g3 #d:#f:b #d:#f:b o|a3 #c:e a3 #c:e e:#g:b #d:#f:b #c:e:#g b3:e:#g|a2 #c:e a3 #c:e|b3-:#f b3-:#f o- b2- b2 b3|#f3 #g:b e #g:b|b3 #d:#f #d:#f o|#c3 e:#g #c e:#g|#g3 #d:#f:b #d:#f:b o|a3 #c:e a3 #c:e|e:#g:b #d:#f:b #c:e:#g b3:e:#g|#f3 #f:a #c #f:a|b3+:#f b2 b3|e3++ 3 | -------------------------------------------------------------------------------- /.github/workflows/DependencySubmission.yml: -------------------------------------------------------------------------------- 1 | name: DependencySubmission 2 | on: 3 | workflow_dispatch 4 | 5 | jobs: 6 | build: 7 | name: Dependencies 8 | runs-on: ubuntu-latest 9 | permissions: # The Dependency Submission API requires write permission 10 | contents: write 11 | steps: 12 | - name: 'Checkout Repository' 13 | uses: actions/checkout@v3 14 | 15 | - name: Run snapshot action 16 | uses: mikepenz/gradle-dependency-submission@{latest} 17 | with: 18 | gradle-project-path: "MiraiMidiProduce" 19 | gradle-build-module: |- 20 | :core 21 | :service 22 | :bot 23 | -------------------------------------------------------------------------------- /service/src/main/resources/application.conf.default: -------------------------------------------------------------------------------- 1 | service { 2 | deployment { 3 | host = 0.0.0.0 # or else it wouldn't work 4 | port = 5827 5 | } 6 | 7 | produce { 8 | debug = true 9 | commandTimeout = 60000 10 | ffmpegConvertCommand = "ffmpeg -i {{input}} -acodec libmp3lame -ab 256k {{output}}" 11 | timidityConvertCommand = "timidity {{input}} -Ow -o {{output}}" 12 | mscoreConvertMidi2Mp3Command = "musescore {{input}} -o {{output}}" 13 | mscoreConvertMidi2MSCZCommand = "musescore {{input}} -o {{output}}" 14 | mscoreConvertMSCZ2PDFCommand = "musescore {{input}} -o {{output}}" 15 | mscoreConvertMSCZ2PNGSCommand = "musescore {{input}} -o {{output}} --trim-image 120" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | 3 | dependencies { 4 | 5 | api("org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.3.2") 6 | api("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.3.2") 7 | 8 | implementation("com.github.nwaldispuehl:java-lame:v3.98.4") 9 | api("com.github.whiterasbk:mider:beta0.9.19") 10 | api("io.github.mzdluo123:silk4j:1.1-dev") 11 | implementation("org.apache.commons:commons-exec:1.3") 12 | api("com.belerweb:pinyin4j:2.5.1") 13 | 14 | api("io.ktor:ktor-client-core:2.0.0") 15 | api("io.ktor:ktor-client-okhttp:2.0.0") 16 | 17 | testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") 18 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") 19 | 20 | } 21 | 22 | tasks.getByName("test") { 23 | useJUnitPlatform() 24 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/org/mider/produce/core/utils/TimeUtils.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.core.utils 2 | 3 | import org.mider.produce.core.Configuration 4 | 5 | fun Long.autoTimeUnit(): String { 6 | return if (this < 1000) { 7 | "${this}ms" 8 | } else if (this in 1000..59999) { 9 | "${ String.format("%.2f", this.toFloat() / 1000) }s" 10 | } else { 11 | "${ this / 60_000 }m${ (this % 60_000) / 1000 }s" 12 | } 13 | } 14 | 15 | suspend fun Configuration.time(block: suspend () -> R): R { 16 | return if (debug) { 17 | val startCountingTime = System.currentTimeMillis() 18 | val r = block() 19 | val useTime = System.currentTimeMillis() - startCountingTime 20 | logger("生成用时: ${useTime.autoTimeUnit()}") 21 | r 22 | } else block() 23 | } 24 | -------------------------------------------------------------------------------- /awesome-melody/do you hear the people sing.midercode: -------------------------------------------------------------------------------- 1 | >g;i=musicbox>a-.g-- | f-. g-- a-. $b-- C a/3 vv e-. d-- e-.f-- c d/3 v $b3/3 a3-. c-- f.- a-- g-. #f-- g-. d-- @f-. e-- e-. f-- g a-. g-- f-. g-- a-. $b-- C a/3vv e-.d-- e-.f-- c d/3v $b3/3 a3-. c-- f-. a-- g/3 #f/3 g/3 $b-.e-- f oo (macro r n1,n2: @[n1]-.@[n2]--) (!r e,e) | (!r a,#g)(!r a,b)(!r C,b)(!r a,C)(!r b,a) (!r @g,a) b o- o-- C-- (!r D,C) (!r b,C) (!r D,C) (!r b,D) (!r C,b) (!r a,b) Ca (repeat 3: a/3:m g/3:m f/3:m) a/3:m g/3:m a/3:m b:m oo (!r E,D ) g-.:C g--:D C-.:E D--:F E:G g/3:E g/3:D g/3:C f-.:b f--:a f-.:b f--:C e:g c/3:a c/3:g c/3:f c-.:e e--:g a-.:C C--:E a-.:D a--:#C #f-.:D #f--:a g-.:@C g--:b g-.:b g--:@C b:D g-.:E g--:D g-.:C g--:D C-.:E D--:F E:G g/3:E g/3:D g/3:C f-.:b f--:a f-.:b f--:C e:g c/3:a c/3:g c/3:f c-.:e c--:g a-.:C a--:E a/3:D a/3:#C a/3:D g-.:F g--:b g+:@C 2 | (# source: https://www.bilibili.com/read/cv14960783) 3 | -------------------------------------------------------------------------------- /cl/build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | plugins { 3 | application 4 | } 5 | 6 | apply(plugin = "io.ktor.plugin") 7 | apply(plugin = "application") 8 | 9 | application { 10 | mainClass.set("org.mider.produce.cl.ClKt") 11 | } 12 | 13 | dependencies { 14 | implementation(project(":core")) 15 | 16 | implementation("info.picocli:picocli:4.7.5") 17 | implementation("com.github.nwaldispuehl:java-lame:v3.98.4") 18 | 19 | testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") 20 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") 21 | } 22 | 23 | tasks.named("jar") { 24 | 25 | manifest { 26 | attributes("Main-Class" to "org.mider.produce.cl.ClKt") 27 | } 28 | 29 | from(configurations.runtimeClasspath.get().map { 30 | if (it.isDirectory) it else zipTree(it) 31 | }) 32 | 33 | duplicatesStrategy = DuplicatesStrategy.INCLUDE 34 | } 35 | 36 | tasks.getByName("test") { 37 | useJUnitPlatform() 38 | } -------------------------------------------------------------------------------- /bot/src/main/kotlin/org/mider/produce/bot/game/logic.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.bot.game 2 | 3 | 4 | import net.mamoe.mirai.event.events.GroupMessageEvent 5 | import net.mamoe.mirai.event.events.MessageEvent 6 | 7 | /** 8 | * @param which gameId 9 | */ 10 | suspend fun MessageEvent.gameStart(which: String) { 11 | when (which) { 12 | "relative-pitch-practice", "rpp" -> { 13 | if (this is GroupMessageEvent) { 14 | group.sendMessage("") 15 | } 16 | } 17 | } 18 | } 19 | 20 | class MutableTriple ( 21 | var first: @UnsafeVariance A, 22 | var second: @UnsafeVariance B, 23 | var third: @UnsafeVariance C 24 | ) { 25 | override fun toString(): String = "($first, $second, $third)" 26 | } 27 | 28 | class DurativeMessageHandler { 29 | val actions = mutableSetOf Boolean>>) -> Boolean>>() 30 | 31 | 32 | 33 | fun handle() { 34 | actions.forEach { 35 | // it.first = it.third(actions) 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/org/mider/produce/core/utils/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.core.utils 2 | 3 | import io.github.mzdluo123.silk4j.SilkCoder 4 | import org.mider.produce.core.Configuration 5 | import whiter.music.mider.code.produceCore 6 | import whiter.music.mider.dsl.playDslInstance 7 | import java.io.File 8 | import java.io.IOException 9 | 10 | @Throws(IOException::class) 11 | internal fun Configuration.audioUtilsPcmToSilk(pcmFile: File, sampleRate: Int, bitRate: Int = 24000): File { 12 | if (!pcmFile.exists() || pcmFile.length() == 0L) { 13 | throw IOException("文件不存在或为空") 14 | } 15 | val silkFile = audioUtilsGetTempFile("silk", false) 16 | SilkCoder.encode(pcmFile.absolutePath, silkFile.absolutePath, sampleRate, bitRate) 17 | return silkFile 18 | } 19 | 20 | fun Configuration.audioUtilsGetTempFile(type: String, autoClean: Boolean = true): File { 21 | val fileName = "mirai_audio_${type}_${System.currentTimeMillis()}.$type" 22 | return File(tmpDir, fileName).let { if (autoClean) it.deleteOnExit(); it } 23 | } 24 | 25 | fun playMiderCodeFile(path: String) { 26 | val r = produceCore(File(path).readText()) 27 | playDslInstance(miderDSL = r.miderDSL) 28 | } -------------------------------------------------------------------------------- /service/build.gradle.kts: -------------------------------------------------------------------------------- 1 | val ktorVersion: String = "2.1.2" 2 | val kotlinVersion: String = "1.6.21" 3 | val logbackVersion: String = "1.4.4" 4 | 5 | plugins { 6 | application 7 | } 8 | 9 | apply(plugin = "io.ktor.plugin") 10 | apply(plugin = "application") 11 | 12 | application { 13 | mainClass.set("org.mider.produce.service.ApplicationKt") 14 | 15 | val isDevelopment: Boolean = project.ext.has("development") 16 | applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") 17 | } 18 | 19 | dependencies { 20 | implementation(project(":core")) 21 | implementation("io.ktor:ktor-server-core-jvm:$ktorVersion") 22 | implementation("io.ktor:ktor-server-netty-jvm:$ktorVersion") 23 | implementation("ch.qos.logback:logback-classic:$logbackVersion") 24 | implementation("io.ktor:ktor-server-content-negotiation-jvm:2.1.2") 25 | implementation("io.ktor:ktor-serialization-jackson-jvm:2.1.2") 26 | 27 | testImplementation("io.ktor:ktor-server-tests-jvm:$ktorVersion") 28 | testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion") 29 | 30 | testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") 31 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") 32 | 33 | } 34 | 35 | tasks.getByName("test") { 36 | useJUnitPlatform() 37 | } -------------------------------------------------------------------------------- /service/src/main/kotlin/org/mider/produce/service/data/ServiceParameter.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.service.data 2 | 3 | import kotlinx.serialization.Serializable 4 | import org.mider.produce.core.Configuration 5 | 6 | @Serializable 7 | data class ServiceParameter( 8 | val midercode: String, 9 | var sinsySynAlpha: Float = 0.55f, 10 | var sinsyF0shift: Int = 0, 11 | var sinsyVibpower: Int = 1, 12 | var recursionLimit: Int = 50, 13 | var silkBitsRate: Int = 24000, 14 | var cache: Boolean = false, 15 | var formatMode: String = "internal->java-lame", 16 | var macroUseStrictMode: Boolean = true, 17 | var isBlankReplaceWith0: Boolean = false, 18 | var envMap: Map = emptyMap(), 19 | var quality: Int = 64, 20 | ) { 21 | fun copy(coreCfg: Configuration) { 22 | coreCfg.sinsySynAlpha = sinsySynAlpha 23 | coreCfg.sinsyF0shift = sinsyF0shift 24 | coreCfg.sinsyVibpower = sinsyVibpower 25 | coreCfg.recursionLimit = recursionLimit 26 | coreCfg.silkBitsRate = silkBitsRate 27 | coreCfg.cache = cache 28 | coreCfg.formatMode = formatMode 29 | coreCfg.macroUseStrictMode = macroUseStrictMode 30 | coreCfg.isBlankReplaceWith0 = isBlankReplaceWith0 31 | coreCfg.envMap = envMap 32 | coreCfg.quality = quality 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /awesome-melody/0shoulldbe.nmc: -------------------------------------------------------------------------------- 1 | 2 | #define symbols 3 | : as operator with note/chord : note 4 | - as operator with note - 5 | b as operator with b note 6 | c as letter 7 | 1 as integer 8 | #define symbols 9 | 10 | var Am = >Avvvv+> 11 | var Amaj = Am 12 | var arr = [] 13 | 14 | class note { 15 | var duration = 1 16 | var pitch = 7 17 | var octave = 4 18 | fun len() { 19 | return 1 20 | } 21 | } 22 | 23 | repeat 5 { 24 | >ABCDEFG> 25 | } 26 | 27 | fun (note: note)>v> { 28 | var n = note.clone() 29 | return n 30 | } 31 | 32 | fun (note: note1)>>(char: note2) { 33 | return note2 34 | } 35 | 36 | fun (note: note)>*>(times: int): unit { 37 | note.duration *= int 38 | } 39 | 40 | fun (note: note1)>:>(note: note2): chord { 41 | // stack: pop note1, pop note2 按访问顺序 42 | // 如果在代码里访问了哪个变量就pop哪个 43 | var chord: chord = chord(note1, note2) 44 | // return会push 45 | return chord 46 | } 47 | 48 | if condition { 49 | 50 | } 51 | 52 | for ,, { 53 | 54 | } 55 | 56 | while { 57 | 58 | } 59 | 60 | when { 61 | 62 | } 63 | 64 | var tarcks = 65 | > abce > 66 | > abce > 67 | > abce > 68 | 69 | var notes = 70 | > abce > + 71 | > abce > + 72 | > abce > 73 | 74 | > Am Bm Cm Am7 Bm6 Am7Bm6 A+++B--c++... 75 | (Am)(Bm)(C):A. Aq[app]#B#C abacde#f^ 76 | KA["sas", 1] [your lyric is here] 0 Ooo+ 77 | | ok | A | 78 | > -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/java 3 | { 4 | "name": "Java", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/java:1-11-buster", 7 | 8 | "features": { 9 | "ghcr.io/devcontainers/features/java:1": {}, 10 | "ghcr.io/devcontainers-contrib/features/kotlin-sdkman:2": {}, 11 | "ghcr.io/mikaello/devcontainer-features/kotlinc:1": {}, 12 | "ghcr.io/devcontainers/features/sshd:1": {}, 13 | "ghcr.io/devcontainers-contrib/features/ffmpeg-apt-get:1": {} 14 | }, 15 | 16 | "customizations": { 17 | "vscode": { 18 | "extensions": [ 19 | "GitHub.github-vscode-theme", 20 | "GitHub.vscode-pull-request-github", 21 | "vscjava.vscode-gradle", 22 | "mathiasfrohlich.Kotlin", 23 | "formulahendry.auto-close-tag", 24 | "almenon.arepl", 25 | "fwcd.kotlin", 26 | "ritwickdey.LiveServer", 27 | "Tyriar.lorem-ipsum", 28 | "eamodio.gitlens" 29 | ] 30 | } 31 | } 32 | 33 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 34 | // "forwardPorts": [], 35 | 36 | // Use 'postCreateCommand' to run commands after the container is created. 37 | // "postCreateCommand": "java -version", 38 | 39 | // Configure tool-specific properties. 40 | // "customizations": {}, 41 | 42 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 43 | // "remoteUser": "root" 44 | } 45 | -------------------------------------------------------------------------------- /bot/src/main/resources/melody-list.txt: -------------------------------------------------------------------------------- 1 | canon-part: https://github.moeyy.xyz/https://raw.githubusercontent.com/whiterasbk/MiraiMidiProduce/dev/awesome-melody/canon-part.midercode 2 | do you hear the people sing: https://github.moeyy.xyz/https://raw.githubusercontent.com/whiterasbk/MiraiMidiProduce/dev/awesome-melody/do%20you%20hear%20the%20people%20sing.midercode 3 | 得得得得: https://github.moeyy.xyz/https://raw.githubusercontent.com/whiterasbk/MiraiMidiProduce/dev/awesome-melody/dou-dizhu.midercode 4 | 两只老虎: https://github.moeyy.xyz/https://raw.githubusercontent.com/whiterasbk/MiraiMidiProduce/dev/awesome-melody/double-tigers.midercode 5 | 外婆的澎湖湾: https://github.moeyy.xyz/https://raw.githubusercontent.com/whiterasbk/MiraiMidiProduce/dev/awesome-melody/grandma's-penghu-bay-part.midercode 6 | 如果当时(片段): https://github.moeyy.xyz/https://raw.githubusercontent.com/whiterasbk/MiraiMidiProduce/dev/awesome-melody/if-at-that-time-part-xusong.midercode 7 | Jingle Bells: https://github.moeyy.xyz/https://raw.githubusercontent.com/whiterasbk/MiraiMidiProduce/dev/awesome-melody/jingle-bells-full.midercode 8 | 校园的早晨: https://github.moeyy.xyz/https://raw.githubusercontent.com/whiterasbk/MiraiMidiProduce/dev/awesome-melody/mornig-in-school.midercode 9 | 惊喜: https://github.moeyy.xyz/https://raw.githubusercontent.com/whiterasbk/MiraiMidiProduce/dev/awesome-melody/surprise-part-jiangwen.midercode 10 | スパークル火花: https://github.moeyy.xyz/https://raw.githubusercontent.com/whiterasbk/MiraiMidiProduce/dev/awesome-melody/%E3%82%B9%E3%83%91%E3%83%BC%E3%82%AF%E3%83%AB%E7%81%AB%E8%8A%B1%20%E5%8A%A8%E7%94%BB%E7%94%B5%E5%BD%B1%E3%80%8A%E4%BD%A0%E7%9A%84%E5%90%8D%E5%AD%97%E3%80%82%E3%80%8B%E6%8F%92%E6%9B%B2.midercode -------------------------------------------------------------------------------- /service/src/main/kotlin/org/mider/produce/service/Application.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.service 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import io.ktor.http.* 5 | import io.ktor.server.config.* 6 | import io.ktor.server.engine.* 7 | import io.ktor.server.netty.* 8 | import org.mider.produce.service.plugins.configureRouting 9 | import org.slf4j.LoggerFactory 10 | import java.io.File 11 | 12 | const val DEFAULT_PORT = 8080 13 | const val DEFAULT_HOST = "0.0.0.0" 14 | 15 | fun main(args: Array) { 16 | 17 | val configPath = args.firstOrNull() 18 | ?: System.getenv("APP_CONFIG") 19 | ?: System.getProperty("config.file") 20 | ?: "application.conf" 21 | 22 | embeddedServer(Netty, environment = applicationEngineEnvironment { 23 | log = LoggerFactory.getLogger("produce.service.ktor.application") 24 | config = HoconApplicationConfig(try { 25 | ConfigFactory.parseFile(File(configPath)) 26 | .withFallback(ConfigFactory.load()) 27 | .resolve() 28 | } catch (e: Exception) { 29 | log.warn("Warning: Could not load config from $configPath, msg: ${e.message}, using default") 30 | ConfigFactory.load() 31 | }) 32 | 33 | module { 34 | configureRouting() 35 | } 36 | 37 | val providePort = config.propertyOrNull("service.deployment.port")?.getString()?.toIntOrNull() 38 | val provideHost = config.propertyOrNull("service.deployment.host")?.getString() 39 | 40 | connector { 41 | port = providePort ?: DEFAULT_PORT 42 | host = provideHost ?: DEFAULT_HOST 43 | } 44 | }).start(true) 45 | } 46 | -------------------------------------------------------------------------------- /core/src/main/kotlin/org/mider/produce/core/init.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.core 2 | 3 | import io.github.mzdluo123.silk4j.AudioUtils 4 | import java.io.File 5 | import java.io.FileFilter 6 | 7 | fun Configuration.initTmpAndFormatTransfer(clz: Any) { 8 | if (!tmpDir.exists()) tmpDir.mkdir() 9 | 10 | // unimportant 11 | val tty = resolveFileAction("2000-years-later.png") 12 | if (!tty.exists()) 13 | clz.javaClass.classLoader.getResourceAsStream("2000-years-later.png")?.let { 14 | tty.writeBytes(it.readAllBytes()) 15 | } ?: info("can not release 2000-years-later.png") 16 | 17 | try { 18 | tmpDir.listFiles(FileFilter { 19 | when(it.extension) { 20 | "so", "dll", "lib", "mp3", "silk", "wave", "wav", "amr", 21 | "mid", "midi", "mscz", "png", "pdf", "pcm", "xml" 22 | -> true 23 | else -> false 24 | } 25 | })?.forEach { 26 | it.delete() 27 | } 28 | } catch (e: Exception) { 29 | error("清理缓存失败") 30 | error(e) 31 | } 32 | 33 | if (formatMode.contains("silk4j")) { 34 | try { 35 | switchToSilk4j(tmpDir) 36 | } catch (e: Exception) { 37 | error("silk4j 加载失败, 将无法生成语音") 38 | error(e) 39 | } 40 | } 41 | 42 | if (formatMode.contains("timidity") && timidityConvertCommand.isBlank()) { 43 | error("timidity 命令未配置, 将无法生成语音(wav)") 44 | } 45 | 46 | if (formatMode.contains("ffmpeg") && ffmpegConvertCommand.isBlank()) { 47 | error("ffmpeg 命令未配置, 将无法生成语音(mp3)") 48 | } 49 | } 50 | 51 | fun switchToSilk4j(tmpDir: File) { 52 | AudioUtils.init(tmpDir) 53 | } -------------------------------------------------------------------------------- /core/src/test/kotlin/org/mider/produce/test/TestLogic.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.test 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import org.junit.jupiter.api.Test 5 | import org.mider.produce.core.Configuration 6 | import org.mider.produce.core.generate 7 | import org.mider.produce.core.utils.toPinyin 8 | import whiter.music.mider.xml.LyricInception 9 | import java.io.File 10 | 11 | class TestLogic { 12 | 13 | val workDir = File("src/test/resources") 14 | val cfg = Configuration(workDir) 15 | 16 | @Test 17 | fun `test core logic`(): Unit = runBlocking { 18 | 19 | LyricInception.replace = { it.toPinyin() } 20 | 21 | val code = """ 22 | 23 | >g;sing:cn> 24 | 5[起]1i.[来]7-[饥]2i-[寒]1i-[交]5-[迫]3-[滴]6+ 25 | [奴]4[隶]0-5-[起]2i.[来]1i-[全]7-[世]6-[界]5- 26 | [受]4-[苦滴]3+[人]5[满]1i.[枪]7-[滴]2i-[热]1i- 27 | [血]5-[已]3-[经]6+[沸]4-[腾]6-[要]2i-[为]1i- 28 | 7[真]2i[理而]4i[斗]71i+[争]1i-O-3i-[l日]2i-[世]7+[界]6-[打] 29 | 7-[个]1i-[落]6-[花]7+[流]5-[水]5-[奴]4#-[隶]5-[们]6.[起]6- 30 | [来]2.[起]1i-7+[来]7-O-2i[不]2i.[要]7-[说]5-[我]5-[们]4#-[—] 31 | 5-[无]3i+[所有]1i-[我]6-[们]7-[要]1i-[做]7[天]2i[下]1i[的]6[主] 32 | 5+[人]5-O-3i.-[这]2i--[是]1i+[最]5.[后]3-[的]6+[斗]4-[争]0-2i.- 33 | [团]1i--[结]7+[起来]6[到]5[明]51\+[天]5\-O-5[英]3i+[特]2i[纳]5[雄]1i[耐] 34 | 07.[尔]7-[就]6.[—]5#-[定]6[要]2i[实]2\i+[现]2\i-0-3i- 35 | [这]2i-[是]1i+[最]5.[后]3-[的]6+[斗]4-[争]0-2.-[团]1-- 36 | [结]7+[起来]6[到]5[明]3i+[天]3i[英]5i+[特]4i[纳]3i[雄维] 37 | 2\i.[耐]3\i-4i[尔]0-4i-[就]3i.[—]3i-[定]2i.[要]2i-[实]1i+[现] 38 | 39 | """.trimIndent() 40 | val generate = cfg.generate(code) 41 | val stream = generate.second.first().first 42 | println(System.getProperty("user.dir")) 43 | File(workDir, "g.mp3").writeBytes(stream.readAllBytes()) 44 | } 45 | } -------------------------------------------------------------------------------- /awesome-melody/listen-one ok rock.midercode: -------------------------------------------------------------------------------- 1 | >200b;#C> 2 | e gee+ gee+ cde d&d+ o 3 | c cdeC+ ba e&e++ o++ 4 | g-a-g-a- C-a-C g-a-g-a- C-a-C 5 | C. D-&D EE+ G+ 6 | g-a-g-a- C-a-C g-a-g-a- C-a-C 7 | C. D-&D ED+ b+ 8 | EEE.D- EEE+ 9 | EEE.E- E-D- DD+ 10 | EEC↑ ~+ AG&G++ 11 | o+oo. 12 | D- EEE.D- EEE+ 13 | EEE.E- ED D.C- 14 | EEC↑ ~+ A.G-&G++ 15 | o++o 16 | g-a-g-a- C-a-C g-a-g-a- C-a-C 17 | C. D-&D EE+ G+ 18 | g-a-g-a- C-a-C g-a-g-a- C-a-C 19 | C. D-&D ED+ b+ 20 | EEE.D- EEE+ 21 | EEE.E- E-D- DD+ 22 | EEC↑ ~+ A.G-&G++ 23 | o+oo- 24 | C-C EEE.D- EEE+ 25 | EEE.E- ED D.C- 26 | EEC↑ ~+ A.G-&G++&G 27 | D E D C 28 | ED-E-EED-E.&E o- 29 | C- C D E D&D oo+ 30 | ED-E-EED-E.&E o- 31 | C- C D E D oo-o+ 32 | C- CDE C↑&~ BAG CDE D&D+ o. 33 | C- CDE C↑&~ BAG CDE D&D oo 34 | C DE+ DE++ 35 | G.A- G.A- G.A- G-F-E 36 | DE+ DE++ 37 | G.A- G.A- G.A- G-F-E 38 | DE+ DE++ 39 | G.A- G.A- G.A- G-F-E C+.o 40 | C-D-C-D-E-D-E 41 | G.A- G.A- G.A- G-F-E 42 | DE+ DD-E.&E+ 43 | EEG+&G Cb+ 44 | DE+ DE.D- C+ 45 | CEG+&G gC+ 46 | DE+DE+. o- 47 | C- CDE D&D+ o+ 48 | DE+DE+.&E-C- 49 | CDE D:G C↑ C↑ A C↑:C CDE C↑ ~ BAG CDE D 50 | C↑C↑BC↑ 51 | CDE C↑ ~ BAG CDE D+ 52 | C↑D↑C↑ 53 | DE+DE++ 54 | G.A- G.A- G.A- G-F-E 55 | C+.EC++ 56 | E.F- E.F- E D↑~~ 57 | DE+DE++ 58 | G.A- G.A- G.A- G-F-E 59 | g-a-g-a- C-a-C g-a-g-a- C-a-C 60 | G.A- G.A- G.A- G-F-E 61 | 62 | >200b;#C> 63 | o A2 E3 A3+ F2 C3 F3+ C3 G3 C4+ G2 D3 G3+ 64 | A2. E3- A3+ F2 C3 F3+ C3 G3 C4+ G2 D3 G3+ 65 | (repeat 30: 66 | A2 E3 A3 E3 67 | F2 C3 F3 C3 68 | C3 G3 C4 G3 69 | G2 D3 G3 D3) 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /service/src/Dockerfile: -------------------------------------------------------------------------------- 1 | # 构建 service 服务镜像 2 | 3 | FROM whiterasbk/musescore:x86_64-3.6.2 4 | 5 | ENV DEBIAN_FRONTEND=noninteractive 6 | ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 7 | ENV PATH=$PATH:$JAVA_HOME/bin 8 | 9 | RUN apt update && \ 10 | apt install -y \ 11 | openjdk-17-jre \ 12 | ffmpeg \ 13 | timidity \ 14 | fluid-soundfont-gm \ 15 | --no-install-recommends && \ 16 | rm -rf /var/lib/apt/lists/* 17 | 18 | WORKDIR /app 19 | 20 | COPY service.jar /app/service.jar 21 | 22 | RUN mkdir -p /app/config /app/config.default 23 | 24 | COPY application.conf /app/config/application.conf 25 | 26 | COPY application.conf /app/config.default/application.conf 27 | 28 | RUN mkdir -p /app/tmp /app/generated 29 | 30 | EXPOSE 5827 31 | 32 | # 创建启动脚本 33 | RUN cat > /app/entrypoint.sh << 'EOF' 34 | #!/bin/bash 35 | set -e 36 | 37 | echo "=== Starting miderproduce-service ===" 38 | 39 | # 检查 config 目录是否为空 40 | if [ -z "$(ls -A /app/config 2>/dev/null)" ]; then 41 | echo "Config directory is empty, copying default configuration..." 42 | 43 | # 复制默认配置 44 | if [ -d "/app/config.default" ] && [ "$(ls -A /app/config.default)" ]; then 45 | cp -rv /app/config.default/* /app/config/ 46 | echo "✅ Default configuration copied successfully" 47 | else 48 | echo "⚠️ Warning: No default configuration found" 49 | fi 50 | else 51 | echo "✅ Using existing configuration" 52 | fi 53 | 54 | # 显示配置文件信息 55 | echo "" 56 | echo "Configuration files:" 57 | ls -lh /app/config/ 58 | 59 | # 检查配置文件是否存在 60 | if [ ! -f "/app/config/application.conf" ]; then 61 | echo "⚠️ Warning: application.conf not found! use default" 62 | fi 63 | 64 | echo "" 65 | echo "=== Starting Java application ===" 66 | echo "Java version:" 67 | java -version 68 | 69 | echo "" 70 | echo "Starting service with config: /app/config/application.conf" 71 | 72 | # 启动应用 73 | exec java -jar /app/service.jar /app/config/application.conf 74 | EOF 75 | 76 | RUN chmod +x /app/entrypoint.sh 77 | 78 | ENTRYPOINT ["/app/entrypoint.sh"] -------------------------------------------------------------------------------- /awesome-melody/偏偏喜欢你.midercode: -------------------------------------------------------------------------------- 1 | >78b;2x;i=musicbox;3;100dB> 2 | A↑ #F↑ D↑+.:D↑↑ E↑:E↑↑ D↑:#F:D↑↑ ~ E↑↑ D↑↑ D↑:B↑ C↑:A↑ B:G↑ B+.:G↑ B:G↑ A↑ G+.:E↑ D↑ D↑↑ B↑ A↑ B↑ B:G↑ G↑-E↑- D↑-^ B↑-v G+ 3 | DbG+. BAGE-D-bD+. ED+ 4 | EDD↑+. G↑ E↑D↑ B-A-G A++. 5 | A#FD↑+.E↑D↑+ E↑D↑ B.A-G++ 6 | GA-G-E+. DD↑BAE D++. 7 | DbG+. BAG E-D-b D+. ED+ 8 | EDD↑+. G↑ E↑D↑ B-A-G A++&A 9 | ABC↑ D↑+. E↑D↑+ E↑D↑ B.A- G++ 10 | G#F E+. D D↑BAAG++. 11 | GG #D+.:#F #F #F+ AG #FE E++&E 12 | B A+. E A+ D↑D↑-E↑- D++.:B 13 | BB E+.:A E:A A+:D↑ AG D+.:G AB++ 14 | E↑E↑-D↑- C↑B A+. G G++ 15 | B+:D↑:G↑ (# ↟) 16 | DbG+. BAG E-D-b D+.ED+ 17 | ED D↑+. G↑E↑D↑ B-A-G A++&A 18 | ABC↑ D↑+. E↑D↑+ E↑D↑ B.A- G++ 19 | G#F E+. D D↑ BAE G++. 20 | GG #D+.:#F #F #F+ AG #FE E++&E 21 | B A+. EA+ D↑D↑-E↑- D++.:B 22 | BB E+.:A E:A A+:D↑ AG D+.:G A B++ 23 | E↑E↑-D↑- C↑B A+. G G++ 24 | B+:D↑:G↑(# ↟) 25 | Db G+. BAG E-D-b D+.ED+ 26 | ED D↑+. G↑ E↑D↑ B-A-G A++&A 27 | ABC↑ D↑+. E↑ D↑+ E↑D↑ B.A-G++ 28 | G#F E+. D D↑BAE G++&G 29 | ABC↑ D↑+. E↑ D↑+ E↑ D↑ B.A- G++ 30 | G#F E+. DD↑BAG G+++ 31 | G-B-D↑-#F↑- G↑- B↑- D↑↑- #F↑↑- 32 | B↑+.:D↑↑:G↑↑(# ↟) 33 | B-A- B++&B+++ 34 | 35 | >g;2x;3;i=musicbox;90dB> 36 | oo b↓ #f b #f b2:b↓ ~ oo e↓ b↓ e d↓+ b↓ #c↓+ c↓ a↓ c+ d↓ a↓ d+ g↓ g-e- d-e- b↓- a↓- g↓+ 37 | oo g↓dbg dgbg d↓a↓d#f ad 38 | #fd e↓ b↓ eg bgeb↓ a↓ eae d↓ a↓ 39 | d#f b↓#faDEDaD e↓b↓eg bg 40 | eb↓ cgC+ a↓ egC | d↓ a↓ d a↓ de 41 | #f+ g↓dbg dgbg d↓a↓d#f ad 42 | #fd e↓b↓eg bgbe a↓eae d↓ 43 | a↓ d#f b↓#faDEDaD e↓b↓eg bg 44 | eb↓ c↓g↓ce d↓a↓d#f g↓dga D++ 45 | b↓#fb#f b↓#fb#f e↓b↓eb↓ d↓b↓gd 46 | c↓g↓ce c↓a↓d#f g↓dgd bdgd #f↓c#fa b↓#fa#f e↓b↓gb↓ e↓b↓eg 47 | b↓eCe d↓d#fa g↓dab g++ 48 | g↓dbg dgbg d↓a↓d#f ad#fd e↓b↓eg bgbe a↓eae d↓a↓d#f 49 | b↓#faDEDaD e↓b↓eg bgeb↓ c↓g↓ce d↓a↓d#f g↓dgb D++ 50 | b↓#fb#f b↓#fb#f e↓b↓eb↓ d↓b↓gd 51 | c↓g↓ce c↓a↓d#f g↓dgd bdgd #f↓c#fa b↓#fa#f e↓b↓gb↓ e↓b↓eg 52 | a↓eCe d↓d#fa g↓dab g++ 53 | g↓dbg dgbg d↓a↓d#f ad#fd e↓b↓eg bgbe a↓eae d↓a↓d#f 54 | b↓#faDEDaD e↓b↓eg bgeb↓ c↓g↓ce d↓a↓d#f g↓dg b&b++ 55 | #f+++:b:D e+++:g:b 56 | a↓e a+ d++:#f:a g↓bD#F g↓++++ 57 | -------------------------------------------------------------------------------- /cl/src/main/kotlin/sinsy.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.cl 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.engine.* 5 | import io.ktor.client.engine.okhttp.* 6 | import io.ktor.client.plugins.* 7 | import io.ktor.client.request.* 8 | import io.ktor.client.request.forms.* 9 | import io.ktor.client.statement.* 10 | import io.ktor.http.* 11 | import org.mider.produce.core.SinsyConfig 12 | import java.io.ByteArrayInputStream 13 | import java.io.File 14 | 15 | 16 | suspend fun sinsyDownload(xmlPath: String, config: SinsyConfig, uploadCallback: ((Long, Long) -> Unit)? = null, proxyHost: String? = null): ByteArrayInputStream { 17 | val client = HttpClient(OkHttp) { 18 | 19 | engine { 20 | proxy = proxyHost?.let { ProxyBuilder.http(it) } 21 | } 22 | 23 | install(HttpTimeout) { 24 | requestTimeoutMillis = config.sinsyClientRequestTimeoutMillis 25 | connectTimeoutMillis = config.sinsyClientConnectTimeoutMillis 26 | socketTimeoutMillis = config.sinsyClientSocketTimeoutMillis 27 | } 28 | } 29 | 30 | val r = client.post { 31 | url("${config.sinsyLink}/index.php") 32 | header("User-Agent", "Mozilla/5.0") 33 | 34 | setBody(MultiPartFormDataContent(formData { 35 | append("SPKR_LANG", config.SPKR_LANG) 36 | append("SPKR", config.SPKR) 37 | append("VIBPOWER", config.VIBPOWER) 38 | append("F0SHIFT", config.F0SHIFT) 39 | append("SYNALPHA", config.SYNALPHA) 40 | val file = File(xmlPath) 41 | append("SYNSRC", file.readBytes(), Headers.build { 42 | append(HttpHeaders.UserAgent, "Mozilla/5.0") 43 | append(HttpHeaders.ContentType, "text/xml") 44 | append(HttpHeaders.Connection, "Keep-Alive") 45 | append(HttpHeaders.ContentDisposition, "filename=\"${file.name}\"") 46 | }) 47 | })) 48 | 49 | if (uploadCallback != null) { 50 | onUpload(uploadCallback) 51 | } 52 | } 53 | 54 | var rFileName: String? = null 55 | for (i in r.bodyAsText().split("temp/")) { 56 | val i1 = i.split(".") 57 | if (i1[1].startsWith("wav")) { 58 | rFileName = Regex("[\\w'\"\\-+#@:,.\\[\\]()]+").find(i1[0])?.value 59 | break 60 | } 61 | } 62 | 63 | if (rFileName == null) error("combine failed, no results found") 64 | return client.get("${config.sinsyLink}/temp/$rFileName.wav").readBytes().inputStream() 65 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/org/mider/produce/core/utils/StringUtils.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.core.utils 2 | 3 | import net.sourceforge.pinyin4j.PinyinHelper 4 | import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType 5 | import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat 6 | import net.sourceforge.pinyin4j.format.HanyuPinyinToneType 7 | import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType 8 | import org.apache.commons.exec.CommandLine 9 | import org.apache.commons.exec.DefaultExecutor 10 | import org.apache.commons.exec.ExecuteWatchdog 11 | import org.apache.commons.exec.PumpStreamHandler 12 | import org.apache.commons.exec.environment.EnvironmentUtils 13 | import org.mider.produce.core.Configuration 14 | import java.io.ByteArrayOutputStream 15 | import java.nio.charset.Charset 16 | 17 | suspend fun String.matchRegex(reg: Regex, block: suspend (String) -> Unit) { 18 | if (this.matches(reg)) { 19 | block(this) 20 | } 21 | } 22 | 23 | fun String.execute(config: Configuration, charset: Charset = Charset.forName("utf-8")): Pair { 24 | 25 | // 设置环境变量 26 | val envMap = EnvironmentUtils.getProcEnvironment() 27 | envMap.putAll(envMap) 28 | 29 | //接收正常结果流 30 | val outputStream = ByteArrayOutputStream() 31 | //接收异常结果流 32 | val errorStream = ByteArrayOutputStream() 33 | val commandline: CommandLine = CommandLine.parse(this) 34 | val exec = DefaultExecutor() 35 | exec.workingDirectory = config.tmpDir 36 | exec.setExitValues(null) 37 | val watchdog = ExecuteWatchdog(config.commandTimeout) 38 | exec.watchdog = watchdog 39 | val streamHandler = PumpStreamHandler(outputStream, errorStream) 40 | exec.streamHandler = streamHandler 41 | exec.execute(commandline) 42 | //不同操作系统注意编码,否则结果乱码 43 | val out = outputStream.toString(charset) 44 | val error = errorStream.toString(charset) 45 | return out to error 46 | } 47 | 48 | fun String.toPinyin(): String { 49 | val pyf = HanyuPinyinOutputFormat() 50 | // 设置大小写 51 | pyf.caseType = HanyuPinyinCaseType.LOWERCASE 52 | // 设置声调表示方法 53 | pyf.toneType = HanyuPinyinToneType.WITH_TONE_NUMBER 54 | // 设置字母u表示方法 55 | pyf.vCharType = HanyuPinyinVCharType.WITH_V 56 | 57 | val sb = StringBuilder() 58 | val regex = Regex("[\\u4E00-\\u9FA5]+") 59 | 60 | for (i in indices) { 61 | // 判断是否为汉字字符 62 | if (regex.matches(this[i].toString())) { 63 | val s = PinyinHelper.toHanyuPinyinStringArray(this[i], pyf) 64 | if (s != null) 65 | sb.append(s[0]) 66 | } else sb.append(this[i]) 67 | } 68 | 69 | return sb.toString() 70 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/org/mider/produce/core/logic.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.core 2 | 3 | import org.mider.produce.core.utils.* 4 | import whiter.music.mider.code.MiderCodeParserConfiguration 5 | import whiter.music.mider.code.NotationType 6 | import whiter.music.mider.code.ProduceCoreResult 7 | import whiter.music.mider.code.produceCore 8 | import whiter.music.mider.dsl.Dsl2MusicXml 9 | import whiter.music.mider.dsl.fromDslInstance 10 | import java.io.BufferedInputStream 11 | import java.io.InputStream 12 | 13 | suspend fun Configuration.generate(code: String, miderCfg: MiderCodeParserConfiguration? = null): Pair>> = time { 14 | info("sounds begin") 15 | 16 | val produceCoreResult = miderCfg?.let { produceCore(code, it) } ?: produceCore(code) 17 | 18 | /* 19 | produceCoreResult的内容: 20 | - 若干控制类变量的新值 21 | - 得到 miderDSL instance 22 | */ 23 | val midiStream: InputStream = fromDslInstance(produceCoreResult.miderDSL).inStream() 24 | 25 | val result = mutableListOf>() 26 | 27 | if (produceCoreResult.isUploadMidi) { 28 | // 上传 midi 29 | result += midiStream to "stream" 30 | } else if (produceCoreResult.isRenderingNotation) { 31 | // 渲染 乐谱 32 | val midi = audioUtilsGetTempFile("mid") 33 | midi.writeBytes(midiStream.readAllBytes()) 34 | 35 | when (produceCoreResult.notationType) { 36 | NotationType.PNGS -> result += convert2PNGS(midi).map { it.inputStream() to it.name } 37 | NotationType.PDF -> result += convert2PDF(midi).let { it.inputStream() to it.name } 38 | NotationType.MSCZ -> result += convert2MSCZ(midi).let { it.inputStream() to it.name } 39 | else -> throw Exception("plz provide the output format") 40 | } 41 | } else if (produceCoreResult.isSing) { 42 | val xmlFile = audioUtilsGetTempFile("xml") 43 | val dsl2MusicXml = Dsl2MusicXml(produceCoreResult.miderDSL) 44 | dsl2MusicXml.save(xmlFile) 45 | 46 | val singer = selectSinger(produceCoreResult.singSong!!.first to produceCoreResult.singSong!!.second) 47 | val sinsyCfg = SinsyConfig( 48 | singer.second, 49 | singer.first, 50 | sinsyVibpower, 51 | sinsyF0shift, 52 | sinsySynAlpha, 53 | sinsyLink, 54 | sinsyClientRequestTimeoutMillis, 55 | sinsyClientConnectTimeoutMillis, 56 | sinsyClientSocketTimeoutMillis 57 | ) 58 | val after = sinsy(xmlFile.absolutePath, sinsyCfg) 59 | result += generateAudioStreamByFormatModeFromWav(BufferedInputStream(after)) to "stream" 60 | } else { 61 | result += generateAudioStreamByFormatMode(midiStream) to "stream" 62 | } 63 | 64 | produceCoreResult to result 65 | } 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | # mpeltonen/sbt-idea plugin 11 | .idea_modules/ 12 | 13 | # JIRA plugin 14 | atlassian-ide-plugin.xml 15 | 16 | # Compiled class file 17 | *.class 18 | 19 | # Log file 20 | *.log 21 | 22 | # BlueJ files 23 | *.ctxt 24 | 25 | # Package Files # 26 | *.jar 27 | *.war 28 | *.nar 29 | *.ear 30 | *.zip 31 | *.tar.gz 32 | *.rar 33 | 34 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 35 | hs_err_pid* 36 | 37 | *~ 38 | 39 | # temporary files which can be created if a process still has a handle open of a deleted file 40 | .fuse_hidden* 41 | 42 | # KDE directory preferences 43 | .directory 44 | 45 | # Linux trash folder which might appear on any partition or disk 46 | .Trash-* 47 | 48 | # .nfs files are created when an open file is removed but is still being accessed 49 | .nfs* 50 | 51 | # General 52 | .DS_Store 53 | .AppleDouble 54 | .LSOverride 55 | 56 | # Icon must end with two \r 57 | Icon 58 | 59 | # Thumbnails 60 | ._* 61 | 62 | # Files that might appear in the root of a volume 63 | .DocumentRevisions-V100 64 | .fseventsd 65 | .Spotlight-V100 66 | .TemporaryItems 67 | .Trashes 68 | .VolumeIcon.icns 69 | .com.apple.timemachine.donotpresent 70 | 71 | # Directories potentially created on remote AFP share 72 | .AppleDB 73 | .AppleDesktop 74 | Network Trash Folder 75 | Temporary Items 76 | .apdisk 77 | 78 | # Windows thumbnail cache files 79 | Thumbs.db 80 | Thumbs.db:encryptable 81 | ehthumbs.db 82 | ehthumbs_vista.db 83 | 84 | # Dump file 85 | *.stackdump 86 | 87 | # Folder config file 88 | [Dd]esktop.ini 89 | 90 | # Recycle Bin used on file shares 91 | $RECYCLE.BIN/ 92 | 93 | # Windows Installer files 94 | *.cab 95 | *.msi 96 | *.msix 97 | *.msm 98 | *.msp 99 | 100 | # Windows shortcuts 101 | *.lnk 102 | 103 | .gradle 104 | build/ 105 | 106 | # Ignore Gradle GUI config 107 | gradle-app.setting 108 | 109 | # Cache of project 110 | .gradletasknamecache 111 | 112 | **/build/ 113 | 114 | # Common working directory 115 | run/ 116 | 117 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 118 | !gradle-wrapper.jar 119 | 120 | 121 | # Local Test Launch point 122 | src/test/kotlin/RunTerminal.kt 123 | 124 | # Mirai console files with direct bootstrap 125 | /config 126 | /data 127 | /plugins 128 | /bots 129 | 130 | # Local Test Launch Point working directory 131 | /debug-sandbox 132 | **/debug-sandbox 133 | RunTerminal.kt 134 | 135 | service/src/test/resources/generated 136 | service/src/test/resources/tmp 137 | 138 | github-package-token 139 | 140 | core/src/test/resources/ 141 | bot/src/test/resources/ 142 | service/src/test/resources/ 143 | generated 144 | .vscode 145 | .chat 146 | 147 | service/generated 148 | service/tmp -------------------------------------------------------------------------------- /bot/src/main/kotlin/org/mider/produce/bot/utils/MessasgeUtils.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.bot.utils 2 | 3 | import net.mamoe.mirai.contact.AudioSupported 4 | import net.mamoe.mirai.event.events.FriendMessageEvent 5 | import net.mamoe.mirai.event.events.GroupMessageEvent 6 | import net.mamoe.mirai.event.events.MessageEvent 7 | import net.mamoe.mirai.message.data.content 8 | import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource 9 | import org.mider.produce.bot.MiderBot 10 | import org.mider.produce.core.Configuration 11 | import org.mider.produce.core.utils.* 12 | import whiter.music.mider.code.produceCore 13 | import whiter.music.mider.dsl.fromDslInstance 14 | import java.io.InputStream 15 | 16 | suspend fun MessageEvent.matchRegex(reg: Regex, block: suspend (String) -> Unit) 17 | = message.content.matchRegex(reg, block) 18 | 19 | suspend fun MessageEvent.matchRegex(reg: String, block: suspend (String) -> Unit) 20 | = matchRegex(Regex(reg), block) 21 | 22 | suspend fun MessageEvent.sendAudioMessage(cfg: Configuration, origin: String, stream: InputStream, attachUploadFileName: String? = null) { 23 | 24 | when (this) { 25 | is GroupMessageEvent -> { 26 | val size = stream.available() 27 | if (size > 1024 * 1024) MiderBot.logger.info("文件大于 1m 可能导致语音无法播放, 大于 upload size 时将自动转为文件上传") 28 | if (size > cfg.uploadSize) { 29 | stream.toExternalResource().use { 30 | group.files.uploadNewFile( 31 | if (attachUploadFileName != null) "$attachUploadFileName-" else "" + 32 | "generate-${System.currentTimeMillis()}.mp3", it 33 | ) 34 | } 35 | } else { 36 | stream.toExternalResource().use { 37 | val audio = group.uploadAudio(it) 38 | group.sendMessage(audio) 39 | if (cfg.cache) MiderBot.cache[origin] = audio 40 | } 41 | } 42 | } 43 | 44 | is FriendMessageEvent -> { 45 | if (stream.available() > cfg.uploadSize) { 46 | friend.sendMessage("生成的语音过大且bot不能给好友发文件") 47 | } else { 48 | stream.toExternalResource().use { 49 | val audio = friend.uploadAudio(it) 50 | friend.sendMessage(audio) 51 | if (cfg.cache) MiderBot.cache[origin] = audio 52 | } 53 | } 54 | } 55 | 56 | else -> throw Exception("打咩") 57 | } 58 | } 59 | 60 | suspend fun AudioSupported.sendMiderCode(cfg: Configuration, code: String) { 61 | val result = produceCore(code, MiderBot.produceCoreConfiguration) 62 | val midiStream = fromDslInstance(result.miderDSL).inStream() 63 | val audioStream = cfg.generateAudioStreamByFormatMode(midiStream) 64 | audioStream.toExternalResource().use { 65 | val audio = uploadAudio(it) 66 | sendMessage(audio) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /awesome-melody/Our Stories BV1cG4y1J7bZ.midercode: -------------------------------------------------------------------------------- 1 | >260b;bD>d5a-d5a-d5a-d5a-#c5+d5a-d5a-d5a-d5a-e5+d5a-d5a-d5a-d5a-e5+#f5a-#f5a-#f5a-d5a-#c5ad5a-a5a-d5a-a5a-#c5ad5a-a5a-d5a-a5a-#c5ad5a-a5a-d5a-a5a-#c5ad.++:#f.++:a.++:d5.++o-d6/3#c6/3a5/3a.++:d5.++a3+d+a.e.d.a3d-&dde#f-ed-#f.a3.d+a.edg-o-#fe-&ede#f-g#f-d.e.d+a.e.d.a3d-&dde#f-ed-#c.a3.:a.a+:a5+#f+:#f5+a++:d5++e++:#f++:e5++OOO*4ad5e5a5-e5-o-d5a-e5d5ad5e5a5-e5-o-d5a-d5e5ad5e5a5-e5-o-d5a-e5d5a:#f5b:g5#f5-b:g5a5-o-a:d5e5#f5-e5ad5e5a5-e5-o-d5a-e5d5ad5e5a5-e5-o-d5a-d5e5ad5e5a5-e5-o-d5a-e5d5a:#f5b:g5#f5-b:g5a5-o-b5a5-e5-d5-aa3:ad:d5e:e5a-:a5-e-:e5-o-d:d5a3-:a-e:e5d:d5a3:ad:d5e:e5a-:a5-e-:e5-o-d:d5a3-:a-d:d5e:e5a3:ad:d5e:e5a-:a5-e-:e5-o-d:d5a3-:a-e:e5d:d5a:#f5b:g5#f5-b:g5a5-o-a:d5e5#f5-e5a3:ad:d5e:e5a-:a5-e-:e5-o-d:d5a3-:a-e:e5d:d5a3:ad:d5e:e5a-:a5-e-:e5-o-d:d5a3-:a-d:d5e:e5a3:ad:d5e:e5a-:a5-e-:e5-o-d:d5a3-:a-e:e5d:d5#f5g5#f5-g5a5-o-d5e5-o#f5e5.+d5&d5++&d5.++#f5+d5+++&d5.++a5+a6.+d6&d6++e6.+a5+g5.#f5.#f5.g5-a5-e5.+d5-e5-a5oa6-e6-d6-a5-d5-a-e3:#coa+a+:d5+a.:a5.a-:e5-oa:d5o-aa-:d5-od5e5#f5-e5d5-#f5.a.a+:d5+a.:a5.a-:e5-od5-b:g5a:#f5a.:e5.d5e5#f5-g:g5#f5-d5.e5.d5+a.:a5.a-:e5-oa:d5o-aa-:d5-oa:d5e5#f5-a:e5a-:d5-#f5.a.a+:d5+a.:a5.a-:e5-od5-b:g5a:#f5a.:e5.b:b5a:a5g5-g.:g5.#f5-g:g5a5.a+:d5+a.:a5.a-:e5-oa:d5o-aa-:d5-od5e5#f5-e5d5-#f5.a.a+:d5+a.:a5.a-:e5-od5-b:g5a:#f5a.:e5.d5e5#f5-g:g5#f5-d5.e5.d5+a.:a5.a-:e5-oa:d5o-aa-:d5-oa:d5e5#f5-a:e5a-:d5-#f5.a.a+:d5+a5.e5-od5-g5#f5e5.oe+:$$a+:$c5+$$e+:g+:$b+#f+:$$b+:d5+d5a-d5a-d5a-d5a-#c5+d5a-d5a-d5a-d5a-e5+d5a-d5a-d5a-d5a-e5+#f5a-#f5a-#f5a-d5a-#c5ad5a-a5a-d5a-a5a-#c5ad5a-a5a-d5a-a5a-#c5ad5a-a5a-d5a-a5a-#c5ad+++:#f+++:a+++:d5+++↟>260b;bD>g3.++:b3.++a3+:#c+:e+b3.++:d.++:#f.++#f3+:a3+g3.++:b3.++a3+:#c+b3.++:d.++:#f.++#f3+:a3+:e+g3:b3:d*6a3:#c:e*2b3:d:#f*6#f3:a3:e*2g3:b3:d*6a3:#c:e*2b1.++:b2.++o-oO*4g1+d3+:a3+*2d3+g2+:d3+d3+:a3+*2d3+#f1+d3+:a3+*3d3+:a3+*4g1+d3+:a3+a1+d3+:e3+b1+#f3+:a3+d2a3-e3#f3.g3++:b3++:d++da3-b3#f.O*4b1+++g3#f-e-aoo.o-*2#f3.e3#f-e-aoo-a3-:d-:g-oa3+:d+:g+g3#f-e-aoo.o-o-#f3.e3#f-d-a.a3-oo-a3+:g+o-g2#f-e-aoOo-#f2.e2#f-e-aoo-a3-:d-:g-oa3+:d+:g+g2#f-e-aoOo-#f2.e2#f-d-e.:a.a3-:g-o-oa3+:g+o-g1:g2b3#f-d-oOo-#f3.:a3.e1:e2b3g-d-oOo-a3.:#c.g1:g2ba-d-oOo-#f3.:a3.e1:e2#f2doOOg1:g2b#f-d-oOo-#f3.:a3.e1:e2bg-d-oOo-a3.:#c.g1:g2ba-d-oO#f3+:a3+e3+:d+oo-a3-:d-o-oa3+:#c+o-g3a3-da3-#f3a3-da3-#f3-a3-de3g3-dg3-d3#f3-d#f3-d3-#f3-dg3a3-da3-#f3a3-da3-#f3-a3-de3g3-dg3-d3#f3-d#f3-d3-#f3-dg3:b3d-ad-#f3:a3d-ad-#f3-:a3-d-ag3:b3d-ad-d3:#f3d-ad-d3:#f3$c3++:#f3++:a3++#c3++:e3++:g3++a3:d:goO#f1oOg1:g2#c3-d3-a3-e3g2.g2-d3-a3-d3.a1:a2d3-e3-a3-e3a2.a2-e3-a3-e3.g1:g2#c3-d3-a3-e3#f2.d3-a3#c.b1:b2#c3-d3-a3-eb2.a3-d-#c-a3-d3-a2-g1:g2#c3-d3-a3-e3g2.g2-d3-a3-d3.a1:a2d3-e3-a3-e3a2.a2-e3-a3-e3.g1:g2#c3-d3-a3-e3#b2.d3-a3#c.b1:b2#c3-d3-a3-eb2.a3-d-#c-a3-d3-a2-g1:g2#c3-d3-a3-e3g2.g2-d3-a3-d3.a1:a2d3-e3-a3-e3a2.a2-e3-a3-e3.g1:g2#c3-d3-a3-e3#f2.d3-a3#c.b1:b2#c3-d3-a3-eb2.a3-d-#c-a3-d3-a2-g1:g2#c3-d3-a3-e3g2.g2-d3-a3-d3.a1:a2d3-e3-a3-e3a2.a2-e3-a3-e3.b2.++:#f3.++Oo-O$$b3+$a3+@g3+g3.++:b3.++a3+:#c+:e+b3.++:d.++:#f.++#f3+:a3+g3.++:b3.++a3+:#c+b3.++:d.++:#f.++#f3+:a3+:e+g3:b3:d*6a3:#c:e*2b3:d:#f*6#f3:a3:e*2g3:b3:d*6a3:#c:e*2b1+++:b2+++ 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /core/src/main/kotlin/org/mider/produce/core/utils/ConverUtils.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.core.utils 2 | 3 | import org.mider.produce.core.Configuration 4 | import java.io.File 5 | import java.io.FileFilter 6 | import java.io.InputStream 7 | 8 | private fun Configuration.convertUsingConfigCommand(usingCommand: String, inputFile: File, outputFile: File): File { 9 | val result = usingCommand 10 | .replace("{{input}}", inputFile.name) 11 | .replace("{{output}}", outputFile.name) 12 | .execute(this) 13 | 14 | if (!outputFile.exists() || outputFile.length() == 0L) throw Exception(result.second) 15 | 16 | ifDebug(result.first) 17 | 18 | return outputFile 19 | } 20 | 21 | private fun Configuration.convertUsingConfigCommand(usingCommand: String, inputFile: File, outputFileExtension: String): File { 22 | return convertUsingConfigCommand(usingCommand, inputFile, audioUtilsGetTempFile(outputFileExtension)) 23 | } 24 | 25 | fun Configuration.timidityConvert(midiFileStream: InputStream): File { 26 | val midiFile = audioUtilsGetTempFile("mid") 27 | midiFile.writeBytes(midiFileStream.readAllBytes()) 28 | val outputFile = audioUtilsGetTempFile("wav") 29 | 30 | val result = timidityConvertCommand 31 | .replace("{{input}}", midiFile.name) 32 | .replace("{{output}}", outputFile.name) 33 | .execute(this) 34 | 35 | if (!outputFile.exists() || outputFile.length() == 0L) throw Exception(result.second) 36 | ifDebug(result.first) 37 | 38 | return outputFile 39 | } 40 | 41 | fun Configuration.convert2PDF(midi: File): File { 42 | return convertUsingConfigCommand(mscoreConvertMSCZ2PDFCommand, midi, "pdf") 43 | } 44 | 45 | fun Configuration.convert2PNGS( midi: File): List { 46 | val outputSample = audioUtilsGetTempFile("png") 47 | outputSample.writeText("大弦嘈嘈如急雨, 小弦切切如私语") 48 | convertUsingConfigCommand(mscoreConvertMSCZ2PNGSCommand, midi, outputSample) 49 | 50 | val result = outputSample.parentFile.listFiles(FileFilter { 51 | it.name.startsWith(outputSample.nameWithoutExtension) && it != outputSample 52 | }) ?: throw Exception("convert to pngs failed") 53 | 54 | return result.toList().sorted() 55 | } 56 | 57 | fun Configuration.ffmpegConvert(midiFileStream: InputStream): File { 58 | val wavFile = audioUtilsGetTempFile("wav") 59 | wavFile.writeBytes(midiFileStream.readAllBytes()) 60 | val outputFile = audioUtilsGetTempFile("mp3") 61 | 62 | val result = ffmpegConvertCommand 63 | .replace("{{input}}", wavFile.name) 64 | .replace("{{output}}", outputFile.name) 65 | .execute(this) 66 | 67 | if (!outputFile.exists() || outputFile.length() == 0L) throw Exception(result.second) 68 | ifDebug(result.first) 69 | 70 | return outputFile 71 | } 72 | 73 | fun Configuration.museScoreConvert( midiFileStream: InputStream): File { 74 | val midiFile = audioUtilsGetTempFile("mid") 75 | midiFile.writeBytes(midiFileStream.readAllBytes()) 76 | return convertUsingConfigCommand(mscoreConvertMidi2Mp3Command, midiFile, "mp3") 77 | } 78 | 79 | fun Configuration.convert2MSCZ(midi: File): File { 80 | return convertUsingConfigCommand(mscoreConvertMidi2MSCZCommand, midi, "mscz") 81 | } -------------------------------------------------------------------------------- /service/src/main/kotlin/org/mider/produce/service/utlis/Utils.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.service.utlis 2 | 3 | import io.ktor.server.application.* 4 | import io.ktor.util.logging.* 5 | import org.mider.produce.core.Configuration 6 | import java.io.File 7 | 8 | const val configPrefix = "service.produce" 9 | const val isDebugging = false 10 | 11 | fun getConfiguration(app: Application): Pair { 12 | 13 | val generatedWorkspace = app.environment.config.propertyOrNull("generatedDir")?.getString()?.let { 14 | val dir = File(it) 15 | if (!dir.exists()) throw Exception("configured generatedDir is not exist") 16 | dir 17 | } ?: run { 18 | val dir = File(System.getProperty("user.dir"), if (isDebugging) "src/test/resources/generated" else "generated") 19 | if (!dir.exists()) dir.mkdir() 20 | dir 21 | } 22 | 23 | val tmpDir = app.environment.config.propertyOrNull("tmpDir")?.getString()?.let { 24 | val dir = File(it) 25 | if (!dir.exists()) throw Exception("configured tmpDir is not exist") 26 | dir 27 | } ?: run { 28 | val dir = File(System.getProperty("user.dir"), if (isDebugging) "src/test/resources/tmp" else "tmp") 29 | if (!dir.exists()) dir.mkdir() 30 | dir 31 | } 32 | 33 | val cfg = Configuration(tmpDir) 34 | 35 | cfg.error = { 36 | when (it) { 37 | is Throwable -> app.log.error(it) 38 | else -> app.log.error(it.toString()) 39 | } 40 | } 41 | 42 | cfg.info = { 43 | app.log.info(it.toString()) 44 | } 45 | 46 | cfg.setupViaEnvironment(app.environment) 47 | 48 | return cfg to generatedWorkspace 49 | } 50 | 51 | 52 | fun Configuration.setupViaEnvironment(env: ApplicationEnvironment) { 53 | 54 | env.longOrDefault("commandTimeout") { commandTimeout = it } 55 | env.stringOrDefault("ffmpegConvertCommand") { ffmpegConvertCommand = it } 56 | env.stringOrDefault("timidityConvertCommand") { timidityConvertCommand = it } 57 | env.stringOrDefault("mscoreConvertMidi2Mp3Command") { mscoreConvertMidi2Mp3Command = it } 58 | env.stringOrDefault("mscoreConvertMidi2MSCZCommand") { mscoreConvertMidi2MSCZCommand = it } 59 | env.stringOrDefault("mscoreConvertMSCZ2PDFCommand") { mscoreConvertMSCZ2PDFCommand = it } 60 | env.stringOrDefault("mscoreConvertMSCZ2PNGSCommand") { mscoreConvertMSCZ2PNGSCommand = it } 61 | env.stringOrDefault("sinsyLink") { sinsyLink = it } 62 | env.booleanOrDefault("debug") { debug = it } 63 | } 64 | 65 | private inline fun ApplicationEnvironment.stringOrDefault(path: String, block: (String) -> Unit) { 66 | config.propertyOrNull("$configPrefix.$path")?.getString()?.let(block) 67 | } 68 | 69 | private inline fun ApplicationEnvironment.intOrDefault(path: String, block: (Int) -> Unit) { 70 | config.propertyOrNull("$configPrefix.$path")?.getString()?.toIntOrNull()?.let(block) 71 | } 72 | 73 | private inline fun ApplicationEnvironment.longOrDefault(path: String, block: (Long) -> Unit) { 74 | config.propertyOrNull("$configPrefix.$path")?.getString()?.toLongOrNull()?.let(block) 75 | } 76 | 77 | private inline fun ApplicationEnvironment.booleanOrDefault(path: String, block: (Boolean) -> Unit) { 78 | config.propertyOrNull("$configPrefix.$path")?.getString()?.toBooleanStrictOrNull()?.let(block) 79 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/org/mider/produce/core/Configuration.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.core 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | import java.io.File 6 | 7 | /** 8 | * @param sinsySynAlpha sinsy 生成语音的声质, -0.8~0.8 9 | * @param sinsyF0shift sinsy 颤音强度, 0.0~2.0 10 | * @param sinsyVibpower sinsy 俯仰变速, -24~24 11 | * @param sinsyLink sinsy 接口 12 | * @param miderCodeFormatName 上传文件的触发格式 13 | * @param selfMockery is2000year 14 | * @param selfMockeryTime 2000year 15 | * @param commandTimeout 命令执行超时时间 16 | * @param ffmpegConvertCommand ffmpeg 转换命令 (不使用 ffmpeg 也可以, 只要能完成 wav 到 mp3 的转换就行, {{input}} 和 {{output}} 由 插件提供不需要修改 17 | * @param timidityConvertCommand timidity 转换命令 (不使用 timidity 也可以, 只要能完成 mid 到 wav 的转换就行 18 | * @param mscoreConvertMidi2Mp3Command score 从 .mid 转换到 .mp3 19 | * @param mscoreConvertMidi2MSCZCommand muse score 从 .mid 转换到 .mscz 20 | * @param mscoreConvertMSCZ2PDFCommand muse score 从 .mid 转换到 .pdf 21 | * @param mscoreConvertMSCZ2PNGSCommand muse score 从 .mid 转换到 .png 序列 22 | * @param recursionLimit include 最大深度 23 | * @param silkBitsRate silk 比特率(吧 24 | * @param cache 是否启用缓存 25 | * @param formatMode 生成模式, 可选的有: internal->java-lame (默认) internal->java-lame->silk4j timidity->ffmpeg timidity->ffmpeg->silk4j timidity->java-lame timidity->java-lame->silk4j muse-score muse-score->silk4j 26 | * @param macroUseStrictMode 宏是否启用严格模式 27 | * @param debug 是否启用调试 28 | * @param isBlankReplaceWith0 是否启用空格替换 29 | * @param quality 量化深度 理论上越大生成 mp3 的质量越好, java-lame 给出的值是 256 30 | * @param uploadSize 超过这个大小则自动改为文件上传 31 | * @param help 帮助信息 (更新版本时记得要删掉这一行) 32 | */ 33 | 34 | data class Configuration ( 35 | var tmpDir: File, // = File(System.getProperty("user.dir")), 36 | var info: (Any) -> Unit = { println(it) }, 37 | var error: (Any) -> Unit = { println(it) }, 38 | var logger: (String) -> Unit = { println(it) }, 39 | var resolveFileAction: (String) -> File = { File(tmpDir, it) }, 40 | var sinsySynAlpha: Float = 0.55f, 41 | var sinsyF0shift: Int = 0, 42 | var sinsyVibpower: Int = 1, 43 | 44 | var sinsyClientRequestTimeoutMillis: Long = 5 * 60_000L, 45 | var sinsyClientConnectTimeoutMillis: Long = 5 * 60_000L, 46 | var sinsyClientSocketTimeoutMillis: Long = 5 * 60_000L, 47 | 48 | // 环境变量表 49 | var envMap: Map = emptyMap(), 50 | 51 | var sinsyLink: String = "http://sinsy.sp.nitech.ac.jp", 52 | var miderCodeFormatName: String = "midercode", 53 | var selfMockeryTime: Long = 7*1000L, 54 | var selfMockery: Boolean = false, 55 | var commandTimeout: Long = 60 * 1000L, 56 | var ffmpegConvertCommand: String = "ffmpeg -i {{input}} -acodec libmp3lame -ab 256k {{output}}", 57 | var timidityConvertCommand: String = "timidity {{input}} -Ow -o {{output}}", 58 | var mscoreConvertMidi2Mp3Command: String = "MuseScore3 {{input}} -o {{output}}", 59 | var mscoreConvertMidi2MSCZCommand: String = "MuseScore3 {{input}} -o {{output}}", 60 | var mscoreConvertMSCZ2PDFCommand: String = "MuseScore3 {{input}} -o {{output}}", 61 | var mscoreConvertMSCZ2PNGSCommand: String = "MuseScore3 {{input}} -o {{output}} --trim-image 120", 62 | var recursionLimit: Int = 50, 63 | var silkBitsRate: Int = 24000, 64 | var cache: Boolean = false, 65 | var formatMode: String = "internal->java-lame", 66 | var macroUseStrictMode: Boolean = true, 67 | var debug: Boolean = false, 68 | var isBlankReplaceWith0: Boolean = false, 69 | var quality: Int = 64, 70 | var uploadSize: Long = 1153433L, 71 | var help: String = "https://github.com/whiterasbk/MiraiMidiProduce/blob/master/README.md" 72 | ) -------------------------------------------------------------------------------- /cl/src/main/kotlin/cl.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.cl 2 | 3 | import kotlinx.coroutines.delay 4 | import kotlinx.coroutines.runBlocking 5 | import picocli.CommandLine 6 | import java.util.* 7 | import javax.sound.midi.MidiSystem 8 | import kotlin.system.exitProcess 9 | 10 | fun main(args: Array) { 11 | if (args.isEmpty()) { 12 | // interactive mode 13 | val cmdRegex = Regex( 14 | ">(g|f|\\d+b)((;[-+b#]?[A-G](min|maj|major|minor)?)|(;\\d)|" + 15 | "(;img)|(;pdf)|(;mscz)|(;sing(:[a-zA-Z-]{2,4})?(:[fm]?\\d+)?)|" + 16 | "(;midi)|(;\\d{1,3}%)|(;/\\d+)|(;\\d+dB)|(;[↑↓]+)|(;\\d+(\\.\\d+)?x)|" + 17 | "(;i=([a-zA-Z-]+|\\d+))|(;\\d/\\d))*>" 18 | ) 19 | 20 | val (cfg, tmp) = getConfiguration() 21 | cfg.info = {} 22 | cfg.error = { 23 | errorPrintln(it.toString()) 24 | } 25 | 26 | tmp.deleteOnExit() 27 | 28 | runBlocking { 29 | val scanner = Scanner(System.`in`) 30 | var currentTrackHead = ">g>" 31 | var proxy: String? = null 32 | val helpMessage = "[mider-info] enter exit to quit, stop to stop current midi sequencer" 33 | val sequencer = MidiSystem.getSequencer() 34 | 35 | sequencer.use { seq -> 36 | while (true) { 37 | print("$currentTrackHead ") 38 | val line = scanner.nextLine().trim() 39 | when { 40 | line == "exit" -> exitProcess(0) 41 | line == "help" -> infoPrint("$helpMessage\n") 42 | line == "stop" -> seq.stop() 43 | line == "" -> continue 44 | line matches cmdRegex -> { 45 | currentTrackHead = line 46 | } 47 | line.startsWith("set ") -> { 48 | val attr = line.removePrefix("set ").split("=") 49 | if (attr.size == 2 && attr.first() == "proxy") { 50 | proxy = attr[1] 51 | } else errorPrintln("[mider-error] wrongly setting attribute") 52 | } 53 | 54 | else -> { 55 | try { 56 | val (result, stream) = cfg.generate( 57 | code = currentTrackHead + line, 58 | sinsyProxy = proxy 59 | ) 60 | 61 | when { 62 | result.isRenderingNotation -> { 63 | errorPrintln("[mider-error] yet supported") 64 | } 65 | 66 | result.isSing -> stream.first().use { 67 | playWav(it.readAllBytes()) 68 | } 69 | 70 | else -> stream.first().use { 71 | seq.setSequence(it) 72 | seq.open() 73 | seq.start() 74 | } 75 | } 76 | } catch (e: Throwable) { 77 | errorPrintln("[mider-error] $e") 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | } else exitProcess(CommandLine(MiderCodeCommandLine()).execute(*args)) 86 | } -------------------------------------------------------------------------------- /awesome-melody/Unwelcome School BV16V4y1A7xD.midercode: -------------------------------------------------------------------------------- 1 | >360b>a3oco#d.e-o#g3a3cdc#d#d-e.og#f@f#d@dcb3#a3b2cd#deod3oa3oco#d.e-o#g3a3cdc#d#d-e.og#f@f#def#fg#g#gaab+o+aoc5o#d5.e5-o#gac5d5c5#d5#d5-e5.og5#f5@f5#d5@d5c5b#abc5d5#d5e5oeoaoc5o#d5.e-o#gac5d5c5#d5#d5-e5.og5#f5@f5#d5e5f5#f5g5#g5#g5a5a5b5o#f#gaoc5o#d5.e5-o#gac5d5c5#d5#d5-e5.og5#f5@f5#d5@d5c5b#abc5d5#d5e5oeoaoc5o#d5.e-o#gac5d5c5#d5#d5-e5.og5#f5@f5#d5e5f5#f5g5#g5#g5a5a5b5oOe5#d5e5f5e5@d5c5d5e5#d5e5f5e5@d5c5baoa5o#g5o@g5o#f5o@f5o#d5oe5oe5#d5e5f5e5@d5c5d5e5#d5e5f5e5@d5c5d5e5oaoboc5o#c5od5of5o#d5oe5#d5e5f5e5@d5c5d5e5#d5e5f5e5@d5c5baoa5o#g5o@g5o#f5o@f5o#d5oe5oe5#d5e5f5e5@d5c5d5e5#d5e5f5e5@d5c5bao#d5;e5c5@d5;$e5c5ac5#d5;e5c5@d5;$e5c5O+a3oco#d.e-o#g3a3cdc#d#d-e.og#f@f#d@dcb3#a3b2cd#deod3oa3oco#d.e-o#g3a3cdc#d#d-e.og#f@f#def#fg#g#gaabe#fgaoc5o#d5.e5-o#gac5d5c5#d5#d5-e5.og5#f5@f5#d5@d5c5b#abc5d5#d5e5oeoa3cdc#d#d-e.oac5d5c5#d5#d5-e5.og5#f5@f5#d5e5f5#f5g5#g5g5a5a5b5oOa5-g5-a5-c6-a5-g5-a5-c6-a5-g5-a5-c6-a5-g5-a5-c6-a5-g5-a5-c6-a5-g5-e5-#d5-@d5-c5-a-g-e-#d-@d-c-d-#d-e-g-a-g-a-c5-d5-#d5-e5-#f5-#g5-a5-c6-d6-#d6.e6.e6e6Oa6-g6-$e6-d6-c6-e6-d6-a5-e6-d6-a5-e6-d6-a5-e6-d6-c6-a5-#g5-a5-e5-c5-#d5-e5-c5-a-#g-a-e-c-d-c-a3-g3-a3g3cg3a3g3cd#dc@dc#defde-a3-c-a3-b3cc#cd#de$e5-:a5-d5-c5-e5-:a5-d5-c5-e5-:a5-d5-c5-e5-:a5-d5-c5-e5-:a5-c5-a5-b5-c6-#g5-a5-e5-c5-#g-a-b-c5-d5-#d5-e5-f5-d5-e5-f5-#g5-a5-b5-c6-#d6-e6-f6-e6-d6-e6-b5-g5-d5-b-c5-b-a-b-c5-#g5-a5-b5-c6-b5-a5-b5-a5-#d5-@d5-c5-d5d5-c5-d5d5-c5-d5e5ac5#d5f5g5a5b5c6d6#d6e6e6e6e6e6+Oe5#d5e5f5e5@d5c5d5e5#d5e5f5e5@d5c5baoa5o#g5o@g5o#f5o@f5o#d5oe5oe5#d5e5f5e5@d5c5d5e5#d5e5f5e5@d5c5d5e5oaoboc5o#c5od5of5o#d5oe5#d5e5f5e5@d5c5d5e5#d5e5f5e5@d5c5baoa5o#g5o@g5o#f5o@f5o#d5oe5oe5#d5e5f5e5@d5c5d5e5#d5e5f5e5@d5c5bao#d5;e5c5@d5;$e5c5ac5#d5;e5c5@d5;$e5c5O+ao#d5;e5c5@d5;$e5c5ac5#d5;e5c5@d5;$e5c5O+>360b>f2of2of2of2oa2oa2oa2oa2od2od2od2od2oc2oc2oc2oc2of2of2of2of2oa2oa2oa2oa2od2od2od2od2oc2c2c2c2c2+o+f2a2:c3c2a2:c3f2a2:c3c2c3a2c3:e3e2c3:e3a2c3:e3e2c3:e3d2f2:#b2d2f2:b2d2f2:b2d2f2:b2c2g2:c3#eg2:c3e2:c3oc2:g2of2a2:c3c2a2:c3f2a2:c3c2c3a2c3:e3e2c3:e3a2c3:e3e2c3:e3d2f2:#b2d2f2:b2d2f2:b2d2f2:b2c2g2:c3#eg2:c3e2:c3oc2:g2of2a2:c3c2a2:c3f2a2:c3c2c3a2c3:e3e2c3:e3a2c3:e3e2c3:e3d2f2:#b2d2f2:b2d2f2:b2d2f2:b2c2g2:c3#eg2:c3e2:c3oc2:g2of2a2:c3c2a2:c3f2a2:c3c2c3a2c3:e3e2c3:e3a2c3:e3e2c3:e3d2f2:#b2d2f2:b2d2f2:b2d2f2:b2c2g2:c3#eg2:c3e2:c3oc2:g2od2f2:b2b1f2:b2d2f2:b2b1f2:b2c2g2:c3#eg2:c3e2:c3oc2:g2of2c3:f3a2c3:f3f2c3:f3a2c3:f3c2a2:c3e2a2:c3c2a2:c3e2a2:c3d2f2:b2b1f2:b2d2f2:b2b1f2:b2c2g2:c3#eg2:c3e2:c3oc2:g2of2c3:f3a2c3:f3f2c3:f3a2c3:f3c2a2:c3e2a2:c3c2a2:c3e2a2:c3d2f2:b2b1f2:b2d2f2:b2b1f2:b2c2g2:c3#eg2:c3e2:c3oc2:g2of2c3:f3a2c3:f3f2c3:f3a2c3:f3c2a2:c3e2a2:c3c2a2:c3e2a2:c3d2f2:b2b1f2:b2d2f2:b2b1f2:b2c2g2:c3#eg2:c3e2:c3oc2:g2of2:c3:f3+OO+O+Oc2#d2#e2f2of2of2.f2-of2a2oa2oa2.a2.od2od2od2.d2-od2c2oc2oc2.c2.of2of2of2.f2-of2a2oa2oa2.a2.od2od2od2.d2-od2c2oc2oc2.c2.of1:f2of1:f2of1:f2.f1:f2-of1:f2a1:a2oa1:a2oa1:a2.a1:a2.od2:d3od2:d3od2:d3.d2:d3-od2:d3c2:c3oc2:c3oc2:c3.c2:c3-oc2:c3f1:f2of1:f2of1:f2.f1:f2-of1:f2a1:a2oa1:a2oa1:a2.a1:a2.od2:d3od2:d3od2:d3.d2:d3-od2:d3c2:c3oc2:c3oc2:c3.c2:c3-oc2:c3f2c3:f3f2c3:f3f2c3:f3f2c3:f3a2c3:f3a2c3:f3a2c3:f3a2c3:f3d2f2:#b2d2f2:b2d2f2:b2d2f2:b2c2g2:c3c2g2:c3c2g2:c3c2g2:c3f2c3:f3f2c3:f3f2c3:f3f2c3:f3a2c3:f3a2c3:f3a2c3:f3a2c3d2f2:#b2d2f2:b2d2f2:b2d2f2:b2c2g2:c3c2g2:c3c2g2:c3c2g2:c3f2c3:f3f2c3:f3f2c3:f3f2c3:f3a2c3:f3a2c3:f3a2c3:f3a2c3:f3d2f2:#b2d2f2:b2d2f2:b2d2f2:b2c2g2:c3c2g2:c3c2g2:c3c2g2:c3f2c3:f3f2c3:f3f2c3:f3f2c3:f3f2c3:f3a2c3:f3a2c3:f3a2c3:f3a2c3:f3d2f2:#b2d2f2:b2d2f2:b2d2f2:b2c2:g2c2:g2c2:g2c2:g2c2:g2+c2g2:c3d2f2:b2b1f2:b2d2f2:b2b1f2:b2c2g2:c3#eg2:c3e2:c3oc2:g2of2c3:f3a2c3:f3f2c3:f3a2c3:f3c2a2:c3e2a2:c3c2a2:c3e2a2:c3d2f2:b2b1f2:b2d2f2:b2b1f2:b2c2g2:c3#eg2:c3e2:c3oc2:g2of2c3:f3a2c3:f3f2c3:f3a2c3:f3c2a2:c3e2a2:c3c2a2:c3e2a2:c3d2f2:b2b1f2:b2d2f2:b2b1f2:b2c2g2:c3#eg2:c3e2:c3oc2:g2of2c3:f3a2c3:f3f2c3:f3a2c3:f3c2a2:c3e2a2:c3c2a2:c3e2a2:c3d2f2:b2b1f2:b2d2f2:b2b1f2:b2c2g2:c3#eg2:c3e2:c3oc2:g2of2:c3:f3+OO+O+Oc2#d2#e2f2:c3:f3+OO+O+O+ 2 | -------------------------------------------------------------------------------- /core/src/main/kotlin/org/mider/produce/core/sinsy.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.core 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.engine.okhttp.* 5 | import io.ktor.client.plugins.* 6 | import io.ktor.client.request.* 7 | import io.ktor.client.request.forms.* 8 | import io.ktor.client.statement.* 9 | import io.ktor.http.* 10 | import java.io.File 11 | import java.io.InputStream 12 | 13 | private val singers = mapOf( 14 | "english" to mapOf( 15 | "female" to listOf(9, 10), 16 | "male" to listOf(11) 17 | ), 18 | "japanese" to mapOf( 19 | "female" to listOf(0, 1, 2, 4, 5, 6, 7), 20 | "male" to listOf(3,8) 21 | ), 22 | "mandarin" to mapOf( 23 | "female" to listOf(12), 24 | "male" to listOf() 25 | ) 26 | ) 27 | 28 | fun selectSinger(info: Pair?): Pair { 29 | fun pick(area: String, pattern: String): Int = 30 | singers[area]?.let { lang -> 31 | lang[if (pattern.startsWith("f")) "female" else "male"]?.let { gender -> 32 | try { 33 | gender[(pattern 34 | .replace("f", "") 35 | .replace("m", "") 36 | .toIntOrNull() ?: throw Exception("convert to int failed")) - 1] 37 | } catch (e: IndexOutOfBoundsException) { 38 | throw Exception("no such singer") 39 | } 40 | } ?: throw Exception("unsupported gender") 41 | } ?: throw Exception("unsupported area") 42 | 43 | return if (info == null) 12 to "mandarin" else when (info.first) { 44 | "cn", "zh", "zh-cn" -> pick("mandarin", info.second) to "mandarin" 45 | "jp" -> pick("japanese", info.second) to "japanese" 46 | "us" -> pick("english", info.second) to "english" 47 | else -> (info.second.toIntOrNull() ?: 12) to "mandarin" 48 | } 49 | } 50 | 51 | private val wavFileNameRegex = Regex("[\\w'\"\\-+#@:,.\\[\\]()]+") 52 | 53 | data class SinsyConfig( 54 | var SPKR_LANG: String, 55 | var SPKR: Int, 56 | var VIBPOWER: Int, 57 | var F0SHIFT: Int, 58 | var SYNALPHA: Float, 59 | val sinsyLink: String, 60 | val sinsyClientRequestTimeoutMillis: Long = 60_000L, 61 | val sinsyClientConnectTimeoutMillis: Long = 60_000L, 62 | val sinsyClientSocketTimeoutMillis: Long = 60_000L 63 | ) 64 | 65 | private fun sinsyClient(cfg: SinsyConfig) = HttpClient(OkHttp) { 66 | install(HttpTimeout) { 67 | requestTimeoutMillis = cfg.sinsyClientRequestTimeoutMillis 68 | connectTimeoutMillis = cfg.sinsyClientConnectTimeoutMillis 69 | socketTimeoutMillis = cfg.sinsyClientSocketTimeoutMillis 70 | } 71 | } 72 | 73 | suspend fun sinsy(xmlPath: String, config: SinsyConfig, uploadCallback: ((Long, Long) -> Unit)? = null): InputStream { 74 | 75 | val r = sinsyClient(config).post { 76 | url("${config.sinsyLink}/index.php") 77 | header("User-Agent", "Mozilla/5.0") 78 | 79 | setBody(MultiPartFormDataContent(formData { 80 | append("SPKR_LANG", config.SPKR_LANG) 81 | append("SPKR", config.SPKR) 82 | append("VIBPOWER", config.VIBPOWER) 83 | append("F0SHIFT", config.F0SHIFT) 84 | append("SYNALPHA", config.SYNALPHA) 85 | val file = File(xmlPath) 86 | append("SYNSRC", file.readBytes(), Headers.build { 87 | append(HttpHeaders.UserAgent, "Mozilla/5.0") 88 | append(HttpHeaders.ContentType, "text/xml") 89 | append(HttpHeaders.Connection, "Keep-Alive") 90 | append(HttpHeaders.ContentDisposition, "filename=\"${file.name}\"") 91 | }) 92 | })) 93 | 94 | if (uploadCallback != null) { 95 | onUpload(uploadCallback) 96 | } 97 | } 98 | 99 | var rFileName: String? = null 100 | for (i in r.bodyAsText().split("temp/")) { 101 | val i1 = i.split(".") 102 | if (i1[1].startsWith("wav")) { 103 | rFileName = wavFileNameRegex.find(i1[0])?.value 104 | break 105 | } 106 | } 107 | 108 | 109 | if (rFileName == null) throw Exception("combine failed, no results found") 110 | return sinsyClient(config).get("${config.sinsyLink}/temp/$rFileName.wav").readBytes().inputStream() 111 | } -------------------------------------------------------------------------------- /.github/workflows/detekt.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow performs a static analysis of your Kotlin source code using 7 | # Detekt. 8 | # 9 | # Scans are triggered: 10 | # 1. On every push to default and protected branches 11 | # 2. On every Pull Request targeting the default branch 12 | # 3. On a weekly schedule 13 | # 4. Manually, on demand, via the "workflow_dispatch" event 14 | # 15 | # The workflow should work with no modifications, but you might like to use a 16 | # later version of the Detekt CLI by modifing the $DETEKT_RELEASE_TAG 17 | # environment variable. 18 | name: Scan with Detekt 19 | 20 | on: 21 | # Triggers the workflow on push or pull request events but only for default and protected branches 22 | push: 23 | branches: [ "dev" ] 24 | pull_request: 25 | branches: [ "dev" ] 26 | # schedule: 27 | # - cron: '18 12 * * 3' 28 | 29 | # Allows you to run this workflow manually from the Actions tab 30 | workflow_dispatch: 31 | 32 | env: 33 | # Release tag associated with version of Detekt to be installed 34 | # SARIF support (required for this workflow) was introduced in Detekt v1.15.0 35 | DETEKT_RELEASE_TAG: v1.15.0 36 | 37 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 38 | jobs: 39 | # This workflow contains a single job called "scan" 40 | scan: 41 | name: Scan 42 | # The type of runner that the job will run on 43 | runs-on: ubuntu-latest 44 | 45 | # Steps represent a sequence of tasks that will be executed as part of the job 46 | steps: 47 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 48 | - uses: actions/checkout@v3 49 | 50 | # Gets the download URL associated with the $DETEKT_RELEASE_TAG 51 | - name: Get Detekt download URL 52 | id: detekt_info 53 | env: 54 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | run: | 56 | gh api graphql --field tagName=$DETEKT_RELEASE_TAG --raw-field query=' 57 | query getReleaseAssetDownloadUrl($tagName: String!) { 58 | repository(name: "detekt", owner: "detekt") { 59 | release(tagName: $tagName) { 60 | releaseAssets(name: "detekt", first: 1) { 61 | nodes { 62 | downloadUrl 63 | } 64 | } 65 | tagCommit { 66 | oid 67 | } 68 | } 69 | } 70 | } 71 | ' 1> gh_response.json 72 | 73 | DETEKT_RELEASE_SHA=$(jq --raw-output '.data.repository.release.releaseAssets.tagCommit.oid' gh_response.json) 74 | if [ $DETEKT_RELEASE_SHA != $EXPECTED_DETEKT_RELEASE_SHA ]; then 75 | echo "Release tag doesn't match expected commit SHA" 76 | exit 1 77 | fi 78 | 79 | DETEKT_DOWNLOAD_URL=$(jq --raw-output '.data.repository.release.releaseAssets.nodes[0].downloadUrl' gh_response.json) 80 | echo "download_url=$DETEKT_DOWNLOAD_URL" >> $GITHUB_OUTPUT 81 | 82 | # Sets up the detekt cli 83 | - name: Setup Detekt 84 | run: | 85 | dest=$( mktemp -d ) 86 | curl --request GET \ 87 | --url ${{ steps.detekt_info.outputs.download_url }} \ 88 | --silent \ 89 | --location \ 90 | --output $dest/detekt 91 | chmod a+x $dest/detekt 92 | echo $dest >> $GITHUB_PATH 93 | 94 | # Performs static analysis using Detekt 95 | - name: Run Detekt 96 | continue-on-error: true 97 | run: | 98 | detekt --input ${{ github.workspace }} --report sarif:${{ github.workspace }}/detekt.sarif.json 99 | 100 | # Modifies the SARIF output produced by Detekt so that absolute URIs are relative 101 | # This is so we can easily map results onto their source files 102 | # This can be removed once relative URI support lands in Detekt: https://git.io/JLBbA 103 | - name: Make artifact location URIs relative 104 | continue-on-error: true 105 | run: | 106 | echo "$( 107 | jq \ 108 | --arg github_workspace ${{ github.workspace }} \ 109 | '. | ( .runs[].results[].locations[].physicalLocation.artifactLocation.uri |= if test($github_workspace) then .[($github_workspace | length | . + 1):] else . end )' \ 110 | ${{ github.workspace }}/detekt.sarif.json 111 | )" > ${{ github.workspace }}/detekt.sarif.json 112 | 113 | # Uploads results to GitHub repository using the upload-sarif action 114 | - uses: github/codeql-action/upload-sarif@v2 115 | with: 116 | # Path to SARIF file relative to the root of the repository 117 | sarif_file: ${{ github.workspace }}/detekt.sarif.json 118 | checkout_path: ${{ github.workspace }} 119 | -------------------------------------------------------------------------------- /cl/src/main/kotlin/utils.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.cl 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | import org.mider.produce.core.Configuration 6 | import org.mider.produce.core.SinsyConfig 7 | import org.mider.produce.core.selectSinger 8 | import org.mider.produce.core.utils.* 9 | import whiter.music.mider.code.MiderCodeParserConfiguration 10 | import whiter.music.mider.code.NotationType 11 | import whiter.music.mider.code.ProduceCoreResult 12 | import whiter.music.mider.code.produceCore 13 | import whiter.music.mider.dsl.Dsl2MusicXml 14 | import whiter.music.mider.dsl.fromDslInstance 15 | import whiter.music.mider.xml.LyricInception 16 | import java.io.* 17 | import javax.sound.midi.* 18 | import javax.sound.sampled.* 19 | 20 | 21 | fun getConfiguration(): Pair { 22 | 23 | val workspaceName = ".mider_tmp" 24 | 25 | LyricInception.replace = { 26 | it.toPinyin() 27 | } 28 | 29 | val generatedWorkspace = System.getenv()["MiderTemp"]?.let { 30 | File(it).apply { 31 | if (!exists()) error("tmp folder 'MiderTemp': ($it) is not exist") 32 | } 33 | } ?: File(workspaceName).apply { 34 | if (exists()) { 35 | if (listFiles() != null && listFiles().isNotEmpty()) error("${this.absolutePath} is not empty! consider delete it or point workspace to a empty folder by setting MiderTemp env") 36 | } else mkdir() 37 | } 38 | 39 | val cfg = Configuration(generatedWorkspace) 40 | 41 | cfg.error = { 42 | error("[mider-error] $it") 43 | } 44 | 45 | cfg.info = { 46 | println("[mider-info] $it") 47 | } 48 | 49 | return cfg to generatedWorkspace 50 | } 51 | 52 | suspend fun Configuration.generate( 53 | code: String, 54 | miderCfg: MiderCodeParserConfiguration? = null, 55 | sinsyProxy: String? = null, 56 | sinsyCallback: ((Long, Long) -> Unit)? = null 57 | ): Pair> { 58 | info("sounds begin") 59 | 60 | val produceCoreResult = miderCfg?.let { produceCore(code, it) } ?: produceCore(code) 61 | val midiStream: InputStream = fromDslInstance(produceCoreResult.miderDSL).inStream() 62 | 63 | val stream: List = when { 64 | produceCoreResult.isUploadMidi -> listOf(midiStream) 65 | 66 | produceCoreResult.isRenderingNotation -> { 67 | val midi = audioUtilsGetTempFile("mid") 68 | midi.writeBytes(withContext(Dispatchers.IO) { 69 | midiStream.readAllBytes() 70 | }) 71 | 72 | when (produceCoreResult.notationType) { 73 | NotationType.PNGS -> convert2PNGS(midi).map { it.inputStream() } 74 | NotationType.PDF -> listOf(convert2PDF(midi).inputStream()) 75 | NotationType.MSCZ -> listOf(convert2MSCZ(midi).inputStream()) 76 | else -> { 77 | val errorMsg = "plz provide the output format" 78 | error(errorMsg) 79 | kotlin.error(errorMsg) 80 | } 81 | } 82 | } 83 | 84 | produceCoreResult.isSing -> { 85 | val xmlFile = audioUtilsGetTempFile("xml") 86 | Dsl2MusicXml(produceCoreResult.miderDSL).save(xmlFile) 87 | 88 | val singer = selectSinger(produceCoreResult.singSong!!.first to produceCoreResult.singSong!!.second) 89 | val sinsyCfg = SinsyConfig( 90 | singer.second, 91 | singer.first, 92 | sinsyVibpower, 93 | sinsyF0shift, 94 | sinsySynAlpha, 95 | sinsyLink, 96 | sinsyClientRequestTimeoutMillis, 97 | sinsyClientConnectTimeoutMillis, 98 | sinsyClientSocketTimeoutMillis 99 | ) 100 | 101 | listOf(sinsyDownload(xmlFile.absolutePath, sinsyCfg, sinsyCallback, sinsyProxy)) 102 | } 103 | 104 | else -> listOf(midiStream) // listOf(generateAudioStreamByFormatMode(midiStream)) 105 | } 106 | 107 | return produceCoreResult to stream 108 | } 109 | 110 | fun playWav(wavData: ByteArray) { 111 | val inputStream: InputStream = ByteArrayInputStream(wavData) 112 | val audioInputStream = AudioSystem.getAudioInputStream(inputStream) 113 | val audioFormat = audioInputStream.format 114 | val dataLineInfo = DataLine.Info(SourceDataLine::class.java, audioFormat) 115 | val sourceDataLine = AudioSystem.getLine(dataLineInfo) as SourceDataLine 116 | sourceDataLine.open(audioFormat) 117 | 118 | sourceDataLine.start() 119 | var bytesRead: Int 120 | val buffer = ByteArray(1024) 121 | while (audioInputStream.read(buffer).also { bytesRead = it } != -1) { 122 | sourceDataLine.write(buffer, 0, bytesRead) 123 | } 124 | 125 | sourceDataLine.drain() 126 | sourceDataLine.close() 127 | audioInputStream.close() 128 | } 129 | 130 | fun errorPrint(msg: String) { 131 | print("\u001b[1;31m$msg\u001B[0m") 132 | } 133 | fun errorPrintln(msg: String) = errorPrint("$msg\n") 134 | 135 | fun infoPrint(msg: String) { 136 | print("\u001b[1;33m$msg\u001B[0m") 137 | } 138 | 139 | fun infoPrintln(msg: String) = infoPrint("$msg\n") -------------------------------------------------------------------------------- /bot/src/main/kotlin/org/mider/produce/bot/handle.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.bot 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.engine.okhttp.* 5 | import io.ktor.client.request.* 6 | import io.ktor.client.statement.* 7 | import kotlinx.coroutines.delay 8 | import net.mamoe.mirai.contact.FileSupported 9 | import net.mamoe.mirai.event.events.GroupMessageEvent 10 | import net.mamoe.mirai.event.events.MessageEvent 11 | import net.mamoe.mirai.message.data.FileMessage 12 | import net.mamoe.mirai.message.data.buildMessageChain 13 | import net.mamoe.mirai.message.data.content 14 | import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource 15 | import org.mider.produce.bot.utils.sendAudioMessage 16 | import org.mider.produce.core.Configuration 17 | import org.mider.produce.core.generate 18 | import org.mider.produce.core.utils.ifDebug 19 | import org.mider.produce.core.utils.matchRegex 20 | import whiter.music.mider.cast 21 | import whiter.music.mider.code.MiderCodeParserConfiguration 22 | import whiter.music.mider.code.NotationType 23 | import whiter.music.mider.code.startRegex 24 | 25 | /** 26 | * @param midercode 传入的 midercode 27 | * @param coreCfg 符合 mider 要求的全局配置对象 28 | * @param miderCfg 细分到 midercode解析的配置对象 29 | * @param miderCodeFileName 如果 midercode 来自文件, 则传入后会使用这个文件名生成 mp3 等生成文件格式的名字 30 | */ 31 | suspend fun MessageEvent.handle( 32 | midercode: String, 33 | coreCfg: Configuration, 34 | miderCfg: MiderCodeParserConfiguration, 35 | miderCodeFileName: String? = null 36 | ) { 37 | val cmdRegex = Regex("${startRegex.pattern}[\\S\\s]+") 38 | 39 | midercode.matchRegex(cmdRegex) { msg -> 40 | if (coreCfg.cache && msg in MiderBot.cache) { 41 | MiderBot.cache[msg]?.let { 42 | coreCfg.ifDebug("send from cache") 43 | subject.sendMessage(it) 44 | } ?: throw Exception("启用了缓存但是缓存中没有对应的语音消息") 45 | } else { 46 | val (result, generated) = coreCfg.generate(msg, miderCfg) 47 | val (stream, desc) = generated[0] 48 | 49 | when { 50 | result.isUploadMidi -> stream.toExternalResource().use { 51 | (subject as FileSupported).files.uploadNewFile( 52 | "generate-${System.currentTimeMillis()}.mid", 53 | it 54 | ) 55 | } 56 | 57 | result.isRenderingNotation -> { 58 | when (result.notationType) { 59 | NotationType.PNGS -> { 60 | val chain = buildMessageChain { 61 | generated.forEach { pair -> 62 | val (png, _) = pair 63 | png.toExternalResource().use { 64 | val img = subject.uploadImage(it) 65 | subject.sendMessage(img) 66 | delay(50) 67 | +img 68 | } 69 | } 70 | } 71 | if (coreCfg.cache) MiderBot.cache[msg] = chain 72 | } 73 | 74 | NotationType.PDF -> { 75 | if (subject is FileSupported) { 76 | stream.toExternalResource().use { 77 | (subject as FileSupported).files.uploadNewFile(desc, it) 78 | } 79 | } else subject.sendMessage("打咩") 80 | } 81 | 82 | NotationType.MSCZ -> { 83 | if (subject is FileSupported) { 84 | stream.toExternalResource().use { 85 | (subject as FileSupported).files.uploadNewFile(desc, it) 86 | } 87 | } else subject.sendMessage("打咩") 88 | } 89 | 90 | else -> throw Exception("plz provide the output format") 91 | } 92 | } 93 | 94 | else -> sendAudioMessage(coreCfg, msg, stream, miderCodeFileName) 95 | } 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * 当上传以 .midercode 结尾的名字时候就会触发 102 | */ 103 | suspend fun MessageEvent.handle(coreCfg: Configuration, miderCfg: MiderCodeParserConfiguration) { 104 | var miderCodeFileName: String? = null 105 | val underMsg = if (this is GroupMessageEvent && FileMessage in message) { 106 | val fileMessage = message.find { it is FileMessage }.cast() 107 | if (fileMessage.name.endsWith("." + BotConfiguration.miderCodeFormatName)) { 108 | miderCodeFileName = fileMessage.name.split(".")[0] + "-" 109 | val url = fileMessage.toAbsoluteFile(group)?.getUrl() 110 | val client = HttpClient(OkHttp) 111 | client.get(url ?: throw Exception("current file: ${fileMessage.name} does not exist")).bodyAsText() 112 | } else message.content 113 | } else message.content 114 | 115 | handle(underMsg, coreCfg, miderCfg, miderCodeFileName) 116 | } 117 | -------------------------------------------------------------------------------- /bot/src/main/kotlin/org/mider/produce/bot/BotConfiguration.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.bot 2 | 3 | import net.mamoe.mirai.console.data.AutoSavePluginConfig 4 | import net.mamoe.mirai.console.data.ValueDescription 5 | import net.mamoe.mirai.console.data.value 6 | import org.mider.produce.core.Configuration 7 | 8 | 9 | object BotConfiguration : AutoSavePluginConfig("config") { 10 | @ValueDescription("sinsy 生成语音的声质, -0.8~0.8") 11 | val sinsySynAlpha by value(0.55f) 12 | @ValueDescription("sinsy 颤音强度, 0.0~2.0") 13 | val sinsyF0shift by value(0) 14 | @ValueDescription("sinsy 俯仰变速, -24~24") 15 | val sinsyVibpower by value(1) 16 | @ValueDescription("sinsy 接口") 17 | val sinsyLink by value("http://sinsy.sp.nitech.ac.jp") 18 | 19 | @ValueDescription("sinsyClientRequestTimeoutMillis") 20 | val sinsyClientRequestTimeoutMillis: Long by value(5 * 60_000L) 21 | @ValueDescription("sinsyClientConnectTimeoutMillis") 22 | val sinsyClientConnectTimeoutMillis: Long by value(5 * 60_000L) 23 | @ValueDescription("sinsyClientSocketTimeoutMillis") 24 | val sinsyClientSocketTimeoutMillis: Long by value(5 * 60_000L) 25 | 26 | @ValueDescription("上传文件的触发格式") 27 | val miderCodeFormatName by value("midercode") 28 | @ValueDescription("2000year") 29 | val selfMockeryTime by value(7 * 1000L) 30 | @ValueDescription("is2000year(") 31 | val selfMockery by value(false) 32 | 33 | @ValueDescription("命令执行超时时间") 34 | val commandTimeout by value(60 * 1000L) 35 | 36 | @ValueDescription("ffmpeg 转换命令 (不使用 ffmpeg 也可以, 只要能完成 wav 到 mp3 的转换就行, {{input}} 和 {{output}} 由 插件提供不需要修改") 37 | val ffmpegConvertCommand by value("ffmpeg -i {{input}} -acodec libmp3lame -ab 256k {{output}}") 38 | @ValueDescription("timidity 转换命令 (不使用 timidity 也可以, 只要能完成 mid 到 wav 的转换就行") 39 | val timidityConvertCommand by value("timidity {{input}} -Ow -o {{output}}") 40 | @ValueDescription("muse score 从 .mid 转换到 .mp3 ") 41 | val mscoreConvertMidi2Mp3Command by value("MuseScore3 {{input}} -o {{output}}") 42 | 43 | @ValueDescription("muse score 从 .mid 转换到 .mscz") 44 | val mscoreConvertMidi2MSCZCommand by value("MuseScore3 {{input}} -o {{output}}") 45 | 46 | @ValueDescription("muse score 从 .mid 转换到 .pdf") 47 | val mscoreConvertMSCZ2PDFCommand by value("MuseScore3 {{input}} -o {{output}}") 48 | 49 | @ValueDescription("muse score 从 .mid 转换到 .png 序列") 50 | val mscoreConvertMSCZ2PNGSCommand by value("MuseScore3 {{input}} -o {{output}} --trim-image 120") 51 | 52 | @ValueDescription("include 最大深度") 53 | val recursionLimit by value(50) 54 | @ValueDescription("silk 比特率(吧") 55 | val silkBitsRate by value(24000) 56 | @ValueDescription("是否启用缓存") 57 | val cache by value(true) 58 | 59 | @ValueDescription("生成模式, 可选的有: \n" + 60 | "internal->java-lame (默认)\n" + 61 | "internal->java-lame->silk4j\n" + 62 | "timidity->ffmpeg\n" + 63 | "timidity->ffmpeg->silk4j\n" + 64 | "timidity->java-lame\n" + 65 | "timidity->java-lame->silk4j\n" + 66 | "muse-score\n" + 67 | "muse-score->silk4j\n" 68 | ) 69 | var formatMode by value("internal->java-lame") 70 | @ValueDescription("宏是否启用严格模式") 71 | val macroUseStrictMode by value(true) 72 | @ValueDescription("是否启用调试") 73 | val debug by value(false) 74 | @ValueDescription("是否启用空格替换") 75 | val isBlankReplaceWith0 by value(true) 76 | @ValueDescription("量化深度 理论上越大生成 mp3 的质量越好, java-lame 给出的值是 256") 77 | val quality by value(64) 78 | @ValueDescription("超过这个大小则自动改为文件上传") 79 | val uploadSize by value(1153433L) 80 | @ValueDescription("环境变量表") 81 | val envs by value(emptyMap()) 82 | @ValueDescription("帮助信息 (更新版本时记得要删掉这一行)") 83 | val help by value("https://github.com/whiterasbk/MiraiMidiProduce/blob/master/README.md") 84 | 85 | fun copy(coreCfg: Configuration) { 86 | coreCfg.sinsySynAlpha = sinsySynAlpha 87 | coreCfg.sinsyF0shift = sinsyF0shift 88 | coreCfg.sinsyVibpower = sinsyVibpower 89 | coreCfg.sinsyLink = sinsyLink 90 | 91 | coreCfg.sinsyClientRequestTimeoutMillis = sinsyClientRequestTimeoutMillis 92 | coreCfg.sinsyClientConnectTimeoutMillis = sinsyClientConnectTimeoutMillis 93 | coreCfg.sinsyClientSocketTimeoutMillis = sinsyClientSocketTimeoutMillis 94 | 95 | coreCfg.envMap = envs 96 | 97 | coreCfg.miderCodeFormatName = miderCodeFormatName 98 | coreCfg.selfMockeryTime = selfMockeryTime 99 | coreCfg.selfMockery = selfMockery 100 | coreCfg.commandTimeout = commandTimeout 101 | coreCfg.ffmpegConvertCommand = ffmpegConvertCommand 102 | coreCfg.timidityConvertCommand = timidityConvertCommand 103 | coreCfg.mscoreConvertMidi2Mp3Command = mscoreConvertMidi2Mp3Command 104 | coreCfg.mscoreConvertMidi2MSCZCommand = mscoreConvertMidi2MSCZCommand 105 | coreCfg.mscoreConvertMSCZ2PDFCommand = mscoreConvertMSCZ2PDFCommand 106 | coreCfg.mscoreConvertMSCZ2PNGSCommand = mscoreConvertMSCZ2PNGSCommand 107 | coreCfg.recursionLimit = recursionLimit 108 | coreCfg.silkBitsRate = silkBitsRate 109 | coreCfg.cache = cache 110 | coreCfg.formatMode = formatMode 111 | coreCfg.macroUseStrictMode = macroUseStrictMode 112 | coreCfg.debug = debug 113 | coreCfg.isBlankReplaceWith0 = isBlankReplaceWith0 114 | coreCfg.quality = quality 115 | coreCfg.uploadSize = uploadSize 116 | coreCfg.help = help 117 | } 118 | } -------------------------------------------------------------------------------- /bot/src/test/kotlin/test.kt: -------------------------------------------------------------------------------- 1 | package bot.music.whiter 2 | 3 | import whiter.music.mider.bpm2tempo 4 | import whiter.music.mider.dsl.play 5 | 6 | 7 | suspend fun main() { 8 | 9 | 10 | 11 | 12 | // val k = 13 | // val kj = k.find("20220814162448_9636.wav\">wav, wav, ("http://sinsy.sp.nitech.ac.jp/temp/20220814162448_9636.wav") 34 | // File("tmp.wav").writeBytes(k.readAllBytes()) 35 | 36 | // sinsy("") 37 | // 38 | // val k = produceCore(""" 39 | // >240b;Bmin;i=musicbox>faaabDba | b-D-~~Db-a-a++ | F~~~ GFED | E-~~~EE-F-E++ | FFF-F F-GFED | bDD b-a- a++ | FFF-F F-GFED | a-~~~EC D++-+ 40 | // >240b;Bmin;i=oboe>faaabDba | b-D-~~Db-a-a++ | F~~~ GFED | E-~~~EE-F-E++ | FFF-F F-GFED | bDD b-a- a++ | FFF-F F-GFED | a-~~~EC D++-+ 41 | // """.trimIndent()) 42 | // 43 | // playDslInstance(miderDSL = k.miderDSL) 44 | 45 | 46 | // play { 47 | // O*15; A+D 48 | // debug() 49 | // } 50 | // MiderDSL.instrument.valueOf("piano") 51 | // toInMusicScoreList(""" 52 | // (repeat 16:1↑2↑5↑1↑2↑5↑1↑2↑5↑1↑2↑5↑)(repeat 4:671↑671↑45645651↑3↑51↑3↑572↑572↑) 53 | // (repeat 4:3++0++0++4++0++0++5++0++0++6++005++00)(repeat 4:6006004004001↑001↑00500500) 54 | // 0++033022011000000++000+00333022013003052001000++033042011000000++000+02222032011++000++000++033022011000000++000+00333022013003052001000++033042011000000++000+02222032011+0000++0055555055043021001023022005555055043023000212000001023040032011031020000001023040034050043022032011 55 | // 56 | // """.trimIndent(), useMacro = true, isStave = false).forEach(::println) 57 | 58 | 59 | // println(macro(""" 60 | // (# aa) 61 | // """)) 62 | 63 | // println("4t234".nextOnlyInt(0,3)) 64 | 65 | // play { 66 | // !toMiderNoteList("F+^B\$C6GFG CE\$") 67 | // debug() 68 | 69 | // toMiderNoteListv2("gE+E(def av[x,y] g@x@y)(ui=gCCCD)(ui)Oo~~|A1:A2:A3:A4 (a) ").forEach(::println) 70 | // toMiderNoteListv2("#3i*b", isStave = false).forEach(::println) 71 | 72 | // println(charCount("csacas?c?a", '?')) 73 | // val g = Regex("def\\s+([a-zA-Z_]\\w*)\\s*=\\s*[^>\\s][^>]*") 74 | // val r = g.matchEntire("def _ACCVA=2332saadefacdc def a=0") 75 | // println(r?.groups 76 | // val u = Regex("macro ([a-zA-Z_]\\w*)(\\s*,\\s*[a-zA-Z_]\\w*)*\\s*:\\s*[^>\\s][^>]*") 77 | // val i = u.matchEntire("macro hex,dex,mama,uiu : ni你傻逼哦") 78 | // val test = i?.groupValues 79 | // test?.forEach { 80 | // println(it) 81 | // } 82 | 83 | // C^^^^^^^ 84 | 85 | // play { 86 | // defaultNoteDuration = 1 87 | // !"!A[5,0.25]" 88 | // debug() 89 | // } 90 | 91 | //F+^B'C6GFG CE' F DE'Db' C+ gb' CE' FE' F+ FE' FB' G+ + GB' C6C6B' C6 G+ GE' FGFE' C+ Cb' C+CE'FE'FG E' 92 | 93 | // CoroutineScope(EmptyCoroutineContext).launch { 94 | // val res = HttpClient().get { 95 | // url("https://c.runoob.com/front-end/854/") 96 | // } 97 | // 98 | // println(res.readText()) 99 | // } 100 | 101 | // val f = Regex("[c-gC-GaA]").find("1b1b5566#50O 343vsss;l") 102 | // println(f?.value) 103 | 104 | // val startRegex = Regex(">((g|f|\\d+b)(;([-+b#]?[A-G](min|maj|major|minor)?))?(;\\d)?(;vex|vex&au)?)>") 105 | // val cmdRegex = Regex("${startRegex.pattern}[\\S\\s]+") 106 | // 107 | // val msg = """ 108 | // >g>123 109 | // >g>abc 110 | // >g>5666 111 | // >f>89 112 | // >f;A>WW 113 | // """ 114 | // 115 | // val noteLists = msg.split(startRegex).toMutableList() 116 | // noteLists.removeFirst() 117 | // val configParts = startRegex.findAll(msg).map { it.value.replace(">", "") }.toList() 118 | // 119 | // noteLists.forEachIndexed { index, content -> 120 | // val config = configParts[index] 121 | // println("'$content': $config, $index") 122 | // } 123 | 124 | // println("ffmpeg".execute(charset = "gbk")) 125 | 126 | // play { 127 | // defaultNoteDuration = 2 128 | // !"O*0.1 A[4,1] D[4,1] B[4,1]" 129 | // } 130 | 131 | 132 | // val file = File("debug-sandbox/data/bot.music.whiter.MidiProduce/tmp/mirai_audio_pcm_1653845068124.pcm") 133 | // val s = AudioSystem.getAudioInputStream(file) 134 | // var offset = 0 135 | // val bufferSize = file.length().toInt() 136 | // val audioData = ByteArray(bufferSize) 137 | // 138 | // println(bufferSize) 139 | // 140 | // val af = AudioFormat(44100f, 16, 2, true, false) 141 | // val info = DataLine.Info(SourceDataLine::class.java, af, bufferSize) 142 | // val sdl = AudioSystem.getLine(info) as SourceDataLine 143 | // sdl.open(af) 144 | // sdl.start() 145 | // 146 | // while (offset < audioData.size) { 147 | // offset += sdl.write(audioData, offset, bufferSize) 148 | // } 149 | 150 | 151 | 152 | // play { 153 | // defaultNoteDuration = 1 154 | // bpm = 100 155 | // !toMiderStanderNoteString(toInMusicScoreList(""" 156 | // a d.d- e+v g+ #f++ d.d- e+v a+ v+ d.d- D+b+g+ #f+ e+ C.C- b+ g+^ v+ 157 | // (repeat 3:aaa) 158 | // 159 | // (ew) 160 | // """.trimIndent())) 161 | // 162 | // debug() 163 | // } 164 | 165 | 166 | 167 | // toInMusicScoreList(""" 168 | // D!!! 169 | // """.trimIndent()).forEach(::println) 170 | //macro ([a-zA-Z_]\w*)(\s*,\s*([a-zA-Z_]\w*))*\s*:\s*[^>\s][^>]* 171 | // println(Regex("([a-zA-Z_]\\w*)").pattern) 172 | // println(macro("1234(def a:{233})12(= a )22(android) (if)if (修个小(bug)好难(急着要)不行!()就是)")) 173 | // note().code 174 | // } 175 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/org/mider/produce/core/utils/StreamUtils.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.core.utils 2 | 3 | import io.github.mzdluo123.silk4j.AudioUtils 4 | import net.sourceforge.lame.lowlevel.LameEncoder 5 | import net.sourceforge.lame.mp3.Lame 6 | import net.sourceforge.lame.mp3.MPEGMode 7 | import org.mider.produce.core.* 8 | import whiter.music.mider.dsl.MiderDSL 9 | import whiter.music.mider.dsl.fromDsl 10 | import java.io.ByteArrayInputStream 11 | import java.io.ByteArrayOutputStream 12 | import java.io.File 13 | import java.io.InputStream 14 | import javax.sound.sampled.AudioInputStream 15 | import javax.sound.sampled.AudioSystem 16 | 17 | 18 | fun Configuration.generateAudioStreamByFormatMode(block: MiderDSL.() -> Unit): InputStream 19 | = generateAudioStreamByFormatMode(fromDsl(block).inStream()) 20 | 21 | fun midi2mp3Stream(USE_VARIABLE_BITRATE: Boolean = false, GOOD_QUALITY_BITRATE: Int = 256, midiStream: InputStream): ByteArrayInputStream { 22 | val audioInputStream = AudioSystem.getAudioInputStream(midiStream) 23 | return wave2mp3Stream(audioInputStream, USE_VARIABLE_BITRATE, GOOD_QUALITY_BITRATE) 24 | } 25 | 26 | fun wave2mp3Stream(audioInputStream: AudioInputStream, USE_VARIABLE_BITRATE: Boolean = false, GOOD_QUALITY_BITRATE: Int = 256): ByteArrayInputStream { 27 | val encoder = LameEncoder( 28 | audioInputStream.format, 29 | GOOD_QUALITY_BITRATE, 30 | MPEGMode.STEREO, 31 | Lame.QUALITY_HIGHEST, 32 | USE_VARIABLE_BITRATE 33 | ) 34 | 35 | val mp3 = ByteArrayOutputStream() 36 | val inputBuffer = ByteArray(encoder.pcmBufferSize) 37 | val outputBuffer = ByteArray(encoder.pcmBufferSize) 38 | var bytesRead: Int 39 | var bytesWritten: Int 40 | while (0 < audioInputStream.read(inputBuffer).also { bytesRead = it }) { 41 | bytesWritten = encoder.encodeBuffer(inputBuffer, 0, bytesRead, outputBuffer) 42 | mp3.write(outputBuffer, 0, bytesWritten) 43 | } 44 | 45 | encoder.close() 46 | return ByteArrayInputStream(mp3.toByteArray()) 47 | } 48 | 49 | fun Configuration.generateAudioStreamByFormatModeFromWav(wavStream: InputStream): InputStream { 50 | return when (formatMode) { 51 | "internal->java-lame->silk4j" -> { 52 | val mp3 = wave2mp3Stream(AudioSystem.getAudioInputStream(wavStream), GOOD_QUALITY_BITRATE = quality) 53 | val silk = AudioUtils.mp3ToSilk(mp3, silkBitsRate) 54 | silk.deleteOnExit() 55 | silk.inputStream() 56 | } 57 | 58 | "timidity->ffmpeg" -> { 59 | val mp3 = ffmpegConvert(wavStream) 60 | mp3.inputStream() 61 | } 62 | 63 | "timidity->ffmpeg->silk4j" -> { 64 | val mp3 = ffmpegConvert(wavStream) 65 | val silk = AudioUtils.mp3ToSilk(mp3, silkBitsRate) 66 | silk.deleteOnExit() 67 | silk.inputStream() 68 | } 69 | 70 | else -> { 71 | // internal->java-lame 72 | wave2mp3Stream(AudioSystem.getAudioInputStream(wavStream), GOOD_QUALITY_BITRATE = quality) 73 | } 74 | } 75 | } 76 | 77 | fun Configuration.generateAudioStreamByFormatMode(midiStream: InputStream): InputStream { 78 | return when (formatMode) { 79 | "internal->java-lame->silk4j" -> { 80 | ifDebug("using: internal->java-lame->silk4j") 81 | val audioInputStream = midi2mp3Stream(GOOD_QUALITY_BITRATE = quality, midiStream = midiStream) 82 | val silk = AudioUtils.mp3ToSilk(audioInputStream, silkBitsRate) 83 | silk.deleteOnExit() 84 | silk.inputStream() 85 | } 86 | 87 | "internal->silk4j" -> { 88 | // todo fix sampleRate 和 bitRate 怎么也对不上的问题 89 | ifDebug("using: internal->silk4j") 90 | val pcmStream = AudioSystem.getAudioInputStream(midiStream) 91 | val sampleRate = pcmStream.format.sampleRate.toInt() 92 | val bitRate = pcmStream.format.sampleSizeInBits 93 | val pcmFile = audioUtilsGetTempFile("pcm").let { it.writeBytes(pcmStream.readAllBytes()); it } 94 | audioUtilsPcmToSilk(pcmFile, sampleRate, bitRate).inputStream() 95 | } 96 | 97 | "timidity->ffmpeg" -> { 98 | ifDebug("using: timidity->ffmpeg") 99 | val wav = timidityConvert(midiStream) 100 | val mp3 = ffmpegConvert(wav.inputStream()) 101 | mp3.inputStream() 102 | } 103 | 104 | "timidity->ffmpeg->silk4j" -> { 105 | ifDebug("timidity->ffmpeg->silk4j") 106 | val wav = timidityConvert(midiStream) 107 | val mp3 = ffmpegConvert(wav.inputStream()) 108 | val silk = AudioUtils.mp3ToSilk(mp3, silkBitsRate) 109 | silk.deleteOnExit() 110 | silk.inputStream() 111 | } 112 | 113 | "timidity->silk4j" -> { 114 | ifDebug("using: timidity->silk4j") 115 | TODO("not yet implement: 暂未支持操作") 116 | } 117 | 118 | "timidity->java-lame" -> { 119 | ifDebug("using: timidity->java-lame") 120 | val wav = timidityConvert(midiStream) 121 | wave2mp3Stream(AudioSystem.getAudioInputStream(wav), GOOD_QUALITY_BITRATE = quality) 122 | } 123 | 124 | "timidity->java-lame->silk4j" -> { 125 | ifDebug("using: timidity->java-lame->silk4j") 126 | val wav = timidityConvert(midiStream) 127 | val mp3Stream = wave2mp3Stream(AudioSystem.getAudioInputStream(wav), GOOD_QUALITY_BITRATE = quality) 128 | val silk = AudioUtils.mp3ToSilk(mp3Stream, silkBitsRate) 129 | silk.deleteOnExit() 130 | silk.inputStream() 131 | } 132 | 133 | "muse-score" -> { 134 | ifDebug("using: muse-score") 135 | // support instrument 136 | museScoreConvert(midiStream).inputStream() 137 | } 138 | 139 | "muse-score->silk4j" -> { 140 | ifDebug("using: muse-score->silk4j") 141 | // support instrument 142 | val silk = AudioUtils.mp3ToSilk(museScoreConvert(midiStream, ).inputStream(), silkBitsRate) 143 | silk.deleteOnExit() 144 | silk.inputStream() 145 | } 146 | 147 | // internal->java-lame 或者默认情况 148 | else -> { 149 | ifDebug("using: internal->java-lame") 150 | midi2mp3Stream(GOOD_QUALITY_BITRATE = quality, midiStream = midiStream) 151 | } 152 | } 153 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bot/src/main/kotlin/org/mider/produce/bot/MiderBot.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.bot 2 | 3 | import io.github.mzdluo123.silk4j.AudioUtils 4 | import io.ktor.client.* 5 | import io.ktor.client.engine.okhttp.* 6 | import io.ktor.client.request.* 7 | import io.ktor.client.statement.* 8 | import kotlinx.coroutines.delay 9 | import kotlinx.coroutines.launch 10 | import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription 11 | import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin 12 | import net.mamoe.mirai.event.events.BotEvent 13 | import net.mamoe.mirai.event.events.FriendMessageEvent 14 | import net.mamoe.mirai.event.events.GroupMessageEvent 15 | import net.mamoe.mirai.event.events.MessageEvent 16 | import net.mamoe.mirai.event.globalEventChannel 17 | import net.mamoe.mirai.message.data.Message 18 | import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource 19 | import net.mamoe.mirai.utils.info 20 | import org.mider.produce.bot.game.gameStart 21 | import org.mider.produce.bot.utils.matchRegex 22 | import org.mider.produce.bot.utils.sendAudioMessage 23 | import org.mider.produce.core.Configuration 24 | import org.mider.produce.core.initTmpAndFormatTransfer 25 | import org.mider.produce.core.switchToSilk4j 26 | import org.mider.produce.core.utils.toPinyin 27 | import whiter.music.mider.code.MacroConfiguration 28 | import whiter.music.mider.code.MacroConfigurationBuilder 29 | import whiter.music.mider.code.MiderCodeParserConfiguration 30 | import whiter.music.mider.xml.LyricInception 31 | import java.net.URL 32 | 33 | object MiderBot : KotlinPlugin( 34 | JvmPluginDescription( 35 | id = "org.mider.produce.bot", 36 | name = "MiderBot", 37 | version = "0.1.8", 38 | ) { 39 | author("whiterasbk") 40 | } 41 | ) { 42 | // todo 1. 出场自带 bgm ( 频率 43 | // todo 2. 相对音准小测试 44 | // todo 3. 隔群发送语音, 寻觅知音 ( 45 | // todo 4. 增加乐器( done 46 | // todo 5. 增加力度 done 47 | // todo 6. mider code for js 48 | // todo 7. 权限系统, 话说就发个语音有引入命令权限的必要吗 ( 49 | // todo 8. midi 转 mider code 50 | 51 | val cache = mutableMapOf() 52 | private val tmpDir = resolveDataFile("tmp") 53 | private lateinit var macroConfig: MacroConfiguration 54 | // TODO MiderCodeParserConfiguration.Buider该改改,允许直接 setMacroConfiguration 55 | val produceCoreConfiguration = MiderCodeParserConfiguration() 56 | val cfg = Configuration(tmpDir) 57 | 58 | override fun onEnable() { 59 | 60 | BotConfiguration.reload() 61 | BotConfiguration.copy(cfg) 62 | 63 | cfg.resolveFileAction = ::resolveDataFile 64 | 65 | cfg.initTmpAndFormatTransfer(this) 66 | 67 | cfg.info = { logger.info(it.toString()) } 68 | 69 | cfg.error = { 70 | if (it is Throwable) logger.error(it) else logger.error(it.toString()) 71 | } 72 | 73 | LyricInception.replace = { it.toPinyin() } 74 | 75 | macroConfig = MacroConfigurationBuilder() 76 | .recursionLimit(cfg.recursionLimit) 77 | .loggerError { if (cfg.macroUseStrictMode) throw it else this@MiderBot.logger.error(it) } 78 | .fetchMethod { 79 | if (it.startsWith("http://") || it.startsWith("https://") || it.startsWith("ftp://")) 80 | URL(it).openStream().reader().readText() 81 | else 82 | resolveDataFile(it.replace("file:", "")).readText() 83 | } 84 | .build() 85 | 86 | produceCoreConfiguration.macroConfiguration = macroConfig 87 | produceCoreConfiguration.isBlankReplaceWith0 = BotConfiguration.isBlankReplaceWith0 88 | 89 | val process: suspend MessageEvent.() -> Unit = { 90 | var finishFlag = false 91 | // todo 改为有概率触发 92 | if (BotConfiguration.selfMockery && Math.random() > 0.5) launch { 93 | delay(BotConfiguration.selfMockeryTime) 94 | // 开始嘲讽 95 | if (!finishFlag) { 96 | subject.sendMessage("...") 97 | delay(50) 98 | val tty = resolveDataFile("2000-years-later.png") 99 | if (tty.exists()) { 100 | tty.toExternalResource().use { 101 | subject.sendMessage(subject.uploadImage(it)) 102 | } 103 | } else subject.sendMessage("10 years later.") 104 | } 105 | } 106 | 107 | try { 108 | handle(cfg, produceCoreConfiguration) 109 | oCommandProcess() 110 | } catch (e: Exception) { 111 | logger.error(e) 112 | subject.sendMessage("发生错误>类型:${e::class.simpleName}>" + e.message) 113 | } finally { 114 | finishFlag = true 115 | } 116 | } 117 | 118 | val botEvent = globalEventChannel().filter { it is BotEvent } 119 | 120 | botEvent.subscribeAlways{ 121 | process() 122 | } 123 | 124 | botEvent.subscribeAlways { 125 | process() 126 | } 127 | 128 | logger.info { "MidiProduce loaded" } 129 | } 130 | 131 | private suspend fun MessageEvent.oCommandProcess() { 132 | val oCmdRegex = Regex(">!([\\w@=:&%$#\\->]+)>?") 133 | matchRegex(oCmdRegex) { 134 | val content = oCmdRegex.matchEntire(it)!!.groupValues[1] 135 | if (content == "help") { 136 | subject.sendMessage(BotConfiguration.help) 137 | } else if (content.startsWith("formatMode=")) { 138 | when (val mode = content.replace("formatMode=", "")) { 139 | "internal->java-lame->silk4j", "timidity->ffmpeg", "timidity->ffmpeg->silk4j", "internal->java-lame", 140 | "timidity->java-lame", "timidity->java-lame->silk4j", "muse-score", "muse-score->silk4j" -> { 141 | val before = BotConfiguration.formatMode 142 | BotConfiguration.formatMode = mode 143 | if (mode.contains("silk4j")) switchToSilk4j(tmpDir) 144 | cache.clear() 145 | subject.sendMessage("设置生成模式成功, 由 $before 切换为 $mode") 146 | } 147 | 148 | else -> subject.sendMessage("不支持的模式, 请确认设置的值在以下列表\n" + 149 | "internal->java-lame\n" + 150 | "internal->java-lame->silk4j,\n" + 151 | "timidity->ffmpeg,\n" + 152 | "timidity->ffmpeg->silk4j,\n" + 153 | "timidity->java-lame,\n" + 154 | "timidity->java-lame->silk4j,\n" + 155 | "muse-score,\n" + 156 | "muse-score->silk4j") 157 | } 158 | } else if (content == "clear-cache") { 159 | cache.clear() 160 | subject.sendMessage("cache cleared") 161 | } else if (content == "sample") { 162 | val list = getResource("melody-list.txt") ?: run { 163 | subject.sendMessage("melody-list.txt is not found.") 164 | return@matchRegex 165 | } 166 | val entries = list.lines().map { line -> 167 | val entry = line.split(Regex(": "), 2) 168 | entry[0] to entry[1] 169 | } 170 | val pick = entries.random() 171 | val midercode = HttpClient(OkHttp).get(pick.second).bodyAsText() 172 | subject.sendMessage("now playing: " + pick.first + "\nmidercode: \n$midercode".trim()) 173 | handle(midercode, cfg, produceCoreConfiguration, pick.first + ".mp3") 174 | } else if (content.startsWith("game-start:")) { 175 | gameStart(content.replaceFirst("game-start:", "")) 176 | } 177 | } 178 | } 179 | } 180 | 181 | -------------------------------------------------------------------------------- /cl/src/main/kotlin/MiderCodeCommandLine.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.cl 2 | 3 | import kotlinx.coroutines.delay 4 | import kotlinx.coroutines.runBlocking 5 | import org.mider.produce.core.Configuration 6 | import org.mider.produce.core.utils.generateAudioStreamByFormatMode 7 | import org.mider.produce.core.utils.generateAudioStreamByFormatModeFromWav 8 | import picocli.CommandLine.* 9 | import whiter.music.mider.code.NotationType 10 | import java.io.File 11 | import java.util.concurrent.Callable 12 | import javax.sound.midi.MidiSystem 13 | 14 | 15 | @Command(name = "mccl", mixinStandardHelpOptions = true, version = ["0.1.9"], 16 | description = ["midercode command line tools"]) 17 | class MiderCodeCommandLine : Callable { 18 | 19 | @Parameters(index = "0", description = ["input midercode file, if only this argument provide, the the program will automatically play the midercode"]) 20 | lateinit var file: File 21 | 22 | @Option(names = ["-proxy"], description = ["for internal ktor client"]) 23 | var proxy: String? = null 24 | 25 | @Option(names = ["-o", "-output"], description = ["output file, can be .mid .mp3"]) 26 | var output: String? = null 27 | 28 | @Option(names = ["-sinsyAlpha"], description = ["quality of generated sinsy wave, from -0.8 to 0.8"]) 29 | var sinsySynAlpha: Float? = null 30 | 31 | @Option(names = ["-sinsyF0shift"], description = ["vibrato intensity of generated sinsy wave, from 0.0 to 2.0"]) 32 | var sinsyF0shift: Int? = null 33 | 34 | @Option(names = ["-sinsyVibpower"], description = ["Pitch variable speed of generated sinsy wave, from -24 to 24"]) 35 | var sinsyVibpower: Int? = null 36 | 37 | @Option(names = ["-sinsyClientRequestTimeoutMillis"], description = ["sinsy client request timeout"]) 38 | var sinsyClientRequestTimeoutMillis: Long? = null 39 | 40 | @Option(names = ["-sinsyClientConnectTimeoutMillis"], description = ["sinsy client connect timeout"]) 41 | var sinsyClientConnectTimeoutMillis: Long? = null 42 | 43 | @Option(names = ["-sinsyClientSocketTimeoutMillis"], description = ["sinsy client socket timeout"]) 44 | var sinsyClientSocketTimeoutMillis: Long? = null 45 | 46 | @Option(names = ["-sinsyLink"], description = ["sinsy site link"]) 47 | var sinsyLink: String? = null 48 | 49 | @Option(names = ["-ffmpegConvertCommand"], description = ["ffmpeg convert command"]) 50 | var ffmpegConvertCommand: String? = null 51 | 52 | @Option(names = ["-timidityConvertCommand"], description = ["timidity convert command"]) 53 | var timidityConvertCommand: String? = null 54 | 55 | @Option(names = ["-mscoreConvertMidi2Mp3Command"], description = ["mscore convert Midi to Mp3 command"]) 56 | var mscoreConvertMidi2Mp3Command: String? = null 57 | 58 | @Option(names = ["-mscoreConvertMidi2MSCZCommand"], description = ["mscore convert Midi to MSCZ command"]) 59 | var mscoreConvertMidi2MSCZCommand: String? = null 60 | 61 | @Option(names = ["-mscoreConvertMSCZ2PDFCommand"], description = ["mscore convert MSCZ to PDF command"]) 62 | var mscoreConvertMSCZ2PDFCommand: String? = null 63 | 64 | @Option(names = ["-mscoreConvertMSCZ2PNGSCommand"], description = ["mscore convert MSCZ to PNGS command"]) 65 | var mscoreConvertMSCZ2PNGSCommand: String? = null 66 | 67 | @Option(names = ["-recursionLimit"], description = ["macro recursion limit"]) 68 | var recursionLimit: Int? = null 69 | 70 | @Option(names = ["-silkBitsRate"], description = ["silk bits rate"]) 71 | var silkBitsRate: Int? = null 72 | 73 | @Option(names = ["-fm", "-formatMode"], description = ["format mode, for converting phase"]) 74 | var formatMode: String? = null 75 | 76 | @Option(names = ["-macroUseStrictMode"], description = ["turn on macro strict mode"]) 77 | var macroUseStrictMode: Boolean? = null 78 | 79 | @Option(names = ["-debug"], description = ["debug mode"]) 80 | var debug: Boolean? = null 81 | 82 | @Option(names = ["-isBlankReplaceWith0"], description = ["replace blank with rest note in numeric notation"]) 83 | var isBlankReplaceWith0: Boolean? = null 84 | 85 | @Option(names = ["-quality"], description = ["quality of generated lame format"]) 86 | var quality: Int? = null 87 | 88 | override fun call(): Int = runBlocking { 89 | val (cfg, tmp) = getConfiguration() 90 | 91 | setupConfig(cfg) 92 | 93 | val (result, stream) = cfg.generate( 94 | code = file.readText(), 95 | sinsyCallback = { l1, l2 -> 96 | println("[mider-debug] fetching ... %$l1, %$l2") 97 | }, 98 | sinsyProxy = proxy 99 | ) 100 | 101 | output?.let { 102 | 103 | val file = File(it) 104 | 105 | if (!file.absoluteFile.parentFile.exists()) error("parent file of $file is not exist.") 106 | 107 | when { 108 | it.endsWith(".wav") -> when { 109 | result.isSing -> file.writeBytes(stream.first().readAllBytes()) 110 | // result.isRenderingNotation -> error("visible format isn't able to convert into .wav") 111 | // result.isUploadMidi -> error("midi format isn't able to convert into .wav") 112 | else -> TODO("not support yet") 113 | } 114 | 115 | it.endsWith(".mp3") -> when { 116 | result.isSing -> file.writeBytes(cfg 117 | .generateAudioStreamByFormatModeFromWav(stream.first()).readAllBytes()) 118 | 119 | // result.isRenderingNotation -> error("visible format isn't able to convert into .mp3") 120 | // result.isUploadMidi -> error("midi format isn't able to convert into .mp3") 121 | 122 | else -> file.writeBytes(cfg.generateAudioStreamByFormatMode(stream.first()).readAllBytes()) 123 | } 124 | 125 | it.endsWith(".mid") -> when { 126 | // result.isSing -> error("wave format isn't able to convert into .mid") 127 | // result.isRenderingNotation -> error("visible format isn't able to convert into .mid") 128 | else -> file.writeBytes(stream.first().readAllBytes()) 129 | } 130 | 131 | it.endsWith(".pdf") -> when { 132 | result.isRenderingNotation && result.notationType == NotationType.PDF -> file.writeBytes(stream.first().readAllBytes()) 133 | else -> error("current stream is not stander pdf stream") 134 | } 135 | 136 | it.endsWith(".mscz") -> when { 137 | result.isRenderingNotation && result.notationType == NotationType.MSCZ -> file.writeBytes(stream.first().readAllBytes()) 138 | else -> error("current stream is not stander mscz stream") 139 | } 140 | 141 | it.endsWith(".png") -> when { 142 | result.isRenderingNotation && result.notationType == NotationType.PNGS -> { 143 | if (stream.size == 1) { 144 | file.writeBytes(stream.first().readAllBytes()) 145 | } else { 146 | for ((count, ss) in stream.withIndex()) { 147 | File(file.absoluteFile.parentFile, file.nameWithoutExtension + "-" + (count + 1) + ".png") 148 | .writeBytes(ss.readAllBytes()) 149 | } 150 | } 151 | } 152 | else -> error("current stream is not stander png stream") 153 | } 154 | 155 | else -> error("unsupported format") 156 | } 157 | } ?: when { 158 | result.isRenderingNotation -> { 159 | TODO("not yet supported") 160 | } 161 | 162 | result.isSing -> stream.first().use { 163 | playWav(it.readAllBytes()) 164 | } 165 | 166 | else -> stream.first().use { 167 | val sequencer = MidiSystem.getSequencer() 168 | sequencer.setSequence(it) 169 | sequencer.open() 170 | sequencer.start() 171 | delay(sequencer.sequence.microsecondLength / 1000 + 500) 172 | } 173 | } 174 | 175 | tmp.deleteOnExit() 176 | 177 | 0 178 | } 179 | 180 | private fun setupConfig(cfg: Configuration) { 181 | sinsySynAlpha?.let { cfg.sinsySynAlpha = it } 182 | sinsyF0shift?.let { cfg.sinsyF0shift = it } 183 | sinsyVibpower?.let { cfg.sinsyVibpower = it } 184 | sinsyLink?.let { cfg.sinsyLink = it } 185 | sinsyClientConnectTimeoutMillis?.let { cfg.sinsyClientConnectTimeoutMillis = it } 186 | sinsyClientRequestTimeoutMillis?.let { cfg.sinsyClientRequestTimeoutMillis = it } 187 | sinsyClientSocketTimeoutMillis?.let { cfg.sinsyClientSocketTimeoutMillis = it } 188 | ffmpegConvertCommand?.let { cfg.ffmpegConvertCommand = it } 189 | timidityConvertCommand?.let { cfg.timidityConvertCommand = it } 190 | mscoreConvertMSCZ2PDFCommand?.let { cfg.mscoreConvertMSCZ2PDFCommand = it } 191 | mscoreConvertMSCZ2PNGSCommand?.let { cfg.mscoreConvertMSCZ2PNGSCommand = it } 192 | mscoreConvertMidi2MSCZCommand?.let { cfg.mscoreConvertMidi2MSCZCommand = it } 193 | mscoreConvertMidi2Mp3Command?.let { cfg.mscoreConvertMidi2Mp3Command = it } 194 | recursionLimit?.let { cfg.recursionLimit = it } 195 | silkBitsRate?.let { cfg.silkBitsRate = it } 196 | formatMode?.let { cfg.formatMode = it } 197 | macroUseStrictMode?.let { cfg.macroUseStrictMode = it } 198 | debug?.let { cfg.debug = it } 199 | isBlankReplaceWith0?.let { cfg.isBlankReplaceWith0 = it } 200 | quality?.let { cfg.quality = it } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /.github/workflows/BuildServiceDocker.yml: -------------------------------------------------------------------------------- 1 | name: Service Docker Image Build & Push 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | workflow_dispatch: # 添加手动触发 8 | # release: 9 | # types: [published] 10 | 11 | env: 12 | IMAGE_NAME: miderproduce-service 13 | JAVA_VERSION: '17' 14 | JAVA_DISTRIBUTION: 'temurin' 15 | 16 | jobs: 17 | # ============================================ 18 | # Job 1: 版本信息提取 19 | # ============================================ 20 | extract-version: 21 | name: 📋 Extract Project Version 22 | runs-on: ubuntu-latest 23 | outputs: 24 | version: ${{ steps.get-version.outputs.version }} 25 | docker-tag: ${{ steps.get-version.outputs.docker-tag }} 26 | 27 | steps: 28 | - name: 📥 Checkout Repository 29 | uses: actions/checkout@v4 30 | 31 | - name: 🔍 Parse Version from build.gradle.kts 32 | id: get-version 33 | run: | 34 | VERSION=$(sed -n '/allprojects/,/}/p' build.gradle.kts | grep -E '^\s*version\s*=\s*".*"' | sed -E 's/.*"(.+)".*/\1/') 35 | 36 | if [ -z "$VERSION" ]; then 37 | echo "❌ Failed to extract version from build.gradle.kts" 38 | exit 1 39 | fi 40 | 41 | DOCKER_TAG="${VERSION}" 42 | 43 | echo "📦 Detected Project Version: $VERSION" 44 | echo "🏷️ Docker Tag: $DOCKER_TAG" 45 | 46 | echo "version=$VERSION" >> $GITHUB_OUTPUT 47 | echo "docker-tag=$DOCKER_TAG" >> $GITHUB_OUTPUT 48 | 49 | # ============================================ 50 | # Job 2: Gradle 构建 Fat Jar 51 | # ============================================ 52 | build-jar: 53 | name: 🔨 Build Fat Jar with Gradle 54 | runs-on: ubuntu-latest 55 | needs: extract-version 56 | 57 | steps: 58 | - name: 📥 Checkout Repository with Submodules 59 | uses: actions/checkout@v4 60 | with: 61 | submodules: 'recursive' 62 | 63 | - name: ☕ Setup Java ${{ env.JAVA_VERSION }} 64 | uses: actions/setup-java@v4 65 | with: 66 | java-version: ${{ env.JAVA_VERSION }} 67 | distribution: ${{ env.JAVA_DISTRIBUTION }} 68 | cache: 'gradle' 69 | 70 | - name: 🔧 Grant Execute Permission to Gradlew 71 | run: chmod +x gradlew 72 | 73 | - name: 🏗️ Build Fat Jar (Skip Tests) 74 | env: 75 | GITHUB_USER: ${{ secrets.GITHUB_USER }} 76 | GITHUB_TOKEN: ${{ secrets.GH_PACKAGE_TOKEN }} 77 | run: | 78 | echo "🚀 Starting Gradle build for service module..." 79 | ./gradlew :service:buildFatJar -x test --no-daemon --stacktrace 80 | echo "✅ Gradle build completed successfully" 81 | 82 | - name: ✔️ Verify Fat Jar Exists 83 | run: | 84 | JAR_PATH="service/build/libs/service-all.jar" 85 | if [ ! -f "$JAR_PATH" ]; then 86 | echo "❌ Fat Jar not found at: $JAR_PATH" 87 | exit 1 88 | fi 89 | 90 | JAR_SIZE=$(du -h "$JAR_PATH" | cut -f1) 91 | echo "✅ Fat Jar found: $JAR_PATH (Size: $JAR_SIZE)" 92 | 93 | - name: 📤 Upload Fat Jar as Artifact 94 | uses: actions/upload-artifact@v4 95 | with: 96 | name: service-fat-jar 97 | path: service/build/libs/service-all.jar 98 | retention-days: 1 99 | if-no-files-found: error 100 | 101 | # ============================================ 102 | # Job 3: 准备 Docker 构建上下文 103 | # ============================================ 104 | prepare-docker-context: 105 | name: 📦 Prepare Docker Build Context 106 | runs-on: ubuntu-latest 107 | needs: build-jar 108 | 109 | steps: 110 | - name: 📥 Checkout Repository 111 | uses: actions/checkout@v4 112 | 113 | - name: 📥 Download Fat Jar Artifact 114 | uses: actions/download-artifact@v4 115 | with: 116 | name: service-fat-jar 117 | path: ./artifacts 118 | 119 | - name: 📋 Copy Files to Build Context 120 | run: | 121 | echo "📂 Preparing Docker build context..." 122 | 123 | # 复制 Fat Jar 124 | cp ./artifacts/service-all.jar ./service.jar 125 | echo "✅ Copied service.jar ($(du -h ./service.jar | cut -f1))" 126 | 127 | # 复制 Dockerfile 128 | cp ./service/src/Dockerfile ./Dockerfile 129 | echo "✅ Copied Dockerfile" 130 | 131 | # 复制配置文件 132 | if [ -f "./service/src/main/resources/application.conf.default" ]; then 133 | cp ./service/src/main/resources/application.conf.default ./application.conf 134 | echo "✅ Copied application.conf.default" 135 | else 136 | echo "⚠️ application.conf.default not found, skipping..." 137 | fi 138 | 139 | echo "📦 Build context prepared successfully" 140 | 141 | - name: 📤 Upload Docker Context as Artifact 142 | uses: actions/upload-artifact@v4 143 | with: 144 | name: docker-build-context 145 | path: | 146 | service.jar 147 | Dockerfile 148 | application.conf 149 | retention-days: 1 150 | if-no-files-found: error 151 | 152 | # ============================================ 153 | # Job 4: 构建并推送 Docker 镜像 154 | # ============================================ 155 | build-and-push-docker: 156 | name: 🐳 Build & Push Docker Image 157 | runs-on: ubuntu-latest 158 | environment: docker-image-build 159 | needs: [extract-version, prepare-docker-context] 160 | permissions: 161 | contents: read 162 | packages: write 163 | 164 | steps: 165 | - name: 📥 Download Docker Build Context 166 | uses: actions/download-artifact@v4 167 | with: 168 | name: docker-build-context 169 | path: . 170 | 171 | - name: 🔐 Login to Docker Hub 172 | uses: docker/login-action@v3 173 | with: 174 | username: ${{ secrets.DOCKER_USERNAME }} 175 | password: ${{ secrets.DOCKER_PASSWORD }} 176 | 177 | - name: 🔧 Set up Docker Buildx 178 | uses: docker/setup-buildx-action@v3 179 | 180 | - name: 🏷️ Generate Docker Metadata 181 | id: meta 182 | uses: docker/metadata-action@v5 183 | with: 184 | images: ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }} 185 | tags: | 186 | type=raw,value=${{ needs.extract-version.outputs.docker-tag }} 187 | type=raw,value=latest,enable=${{ github.ref_name == 'dev' }} 188 | 189 | - name: 🐳 Build and Push Docker Image 190 | uses: docker/build-push-action@v5 191 | with: 192 | context: . 193 | push: true 194 | tags: ${{ steps.meta.outputs.tags }} 195 | labels: ${{ steps.meta.outputs.labels }} 196 | cache-from: type=gha 197 | cache-to: type=gha,mode=max 198 | build-args: | 199 | BUILD_DATE=${{ github.event.head_commit.timestamp }} 200 | VCS_REF=${{ github.sha }} 201 | VERSION=${{ needs.extract-version.outputs.version }} 202 | 203 | - name: 📊 Output Image Information 204 | run: | 205 | echo "🎉 Docker image build and push completed!" 206 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" 207 | echo "📦 Repository: ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}" 208 | echo "🏷️ Tags pushed:" 209 | echo "${{ steps.meta.outputs.tags }}" | sed 's/^/ - /' 210 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" 211 | 212 | # ============================================ 213 | # Job 5: 构建总结(可选) 214 | # ============================================ 215 | build-summary: 216 | name: 📝 Build Summary 217 | runs-on: ubuntu-latest 218 | environment: docker-image-build 219 | needs: [extract-version, build-jar, prepare-docker-context, build-and-push-docker] 220 | if: always() 221 | 222 | steps: 223 | - name: 📊 Generate Build Summary 224 | run: | 225 | echo "# 🚀 Docker Image Build Summary" >> $GITHUB_STEP_SUMMARY 226 | echo "" >> $GITHUB_STEP_SUMMARY 227 | echo "| Item | Value |" >> $GITHUB_STEP_SUMMARY 228 | echo "|------|-------|" >> $GITHUB_STEP_SUMMARY 229 | echo "| 📦 Project Version | \`${{ needs.extract-version.outputs.version }}\` |" >> $GITHUB_STEP_SUMMARY 230 | echo "| 🏷️ Docker Tag | \`${{ needs.extract-version.outputs.docker-tag }}\` |" >> $GITHUB_STEP_SUMMARY 231 | echo "| 🐳 Docker Image | \`${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}\` |" >> $GITHUB_STEP_SUMMARY 232 | echo "| 🔀 Branch | \`${{ github.ref_name }}\` |" >> $GITHUB_STEP_SUMMARY 233 | echo "| 📝 Commit | \`${{ github.sha }}\` |" >> $GITHUB_STEP_SUMMARY 234 | echo "| 👤 Triggered By | @${{ github.actor }} |" >> $GITHUB_STEP_SUMMARY 235 | echo "| 🎯 Event Type | \`${{ github.event_name }}\` |" >> $GITHUB_STEP_SUMMARY 236 | echo "" >> $GITHUB_STEP_SUMMARY 237 | echo "## 📋 Job Status" >> $GITHUB_STEP_SUMMARY 238 | echo "" >> $GITHUB_STEP_SUMMARY 239 | echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY 240 | echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY 241 | echo "| Extract Version | ${{ needs.extract-version.result == 'success' && '✅ Success' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY 242 | echo "| Build Jar | ${{ needs.build-jar.result == 'success' && '✅ Success' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY 243 | echo "| Prepare Docker Context | ${{ needs.prepare-docker-context.result == 'success' && '✅ Success' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY 244 | echo "| Build & Push Docker | ${{ needs.build-and-push-docker.result == 'success' && '✅ Success' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY 245 | echo "" >> $GITHUB_STEP_SUMMARY 246 | echo "---" >> $GITHUB_STEP_SUMMARY 247 | echo "🕐 Build completed at: \`$(date -u '+%Y-%m-%d %H:%M:%S UTC')\`" >> $GITHUB_STEP_SUMMARY -------------------------------------------------------------------------------- /service/src/main/kotlin/org/mider/produce/service/plugins/Routing.kt: -------------------------------------------------------------------------------- 1 | package org.mider.produce.service.plugins 2 | 3 | import com.fasterxml.jackson.databind.SerializationFeature 4 | import io.ktor.http.* 5 | import io.ktor.serialization.jackson.* 6 | import io.ktor.server.application.* 7 | import io.ktor.server.http.content.* 8 | import io.ktor.server.plugins.* 9 | import io.ktor.server.plugins.contentnegotiation.* 10 | import io.ktor.server.request.* 11 | import io.ktor.server.response.* 12 | import io.ktor.server.routing.* 13 | import io.ktor.util.logging.* 14 | import io.ktor.util.pipeline.* 15 | import kotlinx.coroutines.Dispatchers 16 | import kotlinx.coroutines.withContext 17 | import org.mider.produce.core.Configuration 18 | import org.mider.produce.core.generate 19 | import org.mider.produce.core.utils.toPinyin 20 | import org.mider.produce.service.data.ResponseBody 21 | import org.mider.produce.service.data.ServiceParameter 22 | import org.mider.produce.service.utlis.getConfiguration 23 | import org.mider.produce.service.utlis.hash 24 | import whiter.music.mider.code.ProduceCoreResult 25 | import whiter.music.mider.xml.LyricInception 26 | import java.io.File 27 | import java.io.InputStream 28 | import java.net.URLDecoder 29 | import java.util.Base64 30 | 31 | fun Application.configureRouting() { 32 | 33 | install(ContentNegotiation) { 34 | jackson { 35 | enable(SerializationFeature.INDENT_OUTPUT) 36 | } 37 | } 38 | 39 | LyricInception.replace = { it.toPinyin() } 40 | 41 | val logger = log 42 | val (config, workspace) = getConfiguration(this) 43 | 44 | // workspace.listFiles()?.forEach { file -> 45 | // if (file.isFile) { 46 | // file.delete() 47 | // } 48 | // } 49 | 50 | routing { 51 | get("/") { 52 | call.respondRedirect("static/index.html") 53 | } 54 | 55 | post("/api") { 56 | handleApiRequest(config, workspace, logger, isDirectApi = false) 57 | } 58 | 59 | post("/direct-api") { 60 | handleApiRequest(config, workspace, logger, isDirectApi = true) 61 | } 62 | 63 | get("/m") { 64 | handleMidercodeRequest(config, workspace, logger, defaultRaw = false) 65 | } 66 | 67 | get("/md") { 68 | handleMidercodeRequest(config, workspace, logger, defaultRaw = true) 69 | } 70 | 71 | get("/generated/{hash}") { 72 | handleFileRequest(workspace, logger) 73 | } 74 | 75 | static("/static") { 76 | resources("static") 77 | } 78 | } 79 | } 80 | 81 | // 提取通用的错误响应函数 82 | private suspend fun PipelineContext.respondError( 83 | statusCode: HttpStatusCode, 84 | code: Int, 85 | message: String, 86 | logger: Logger, 87 | error: Throwable? = null 88 | ) { 89 | call.response.status(statusCode) 90 | call.respond(ResponseBody(code, "failure", message)) 91 | error?.let { logger.error(it) } ?: logger.error(message) 92 | } 93 | 94 | // 提取参数接收逻辑 95 | private suspend fun PipelineContext.receiveParameter( 96 | logger: Logger 97 | ): ServiceParameter? { 98 | return try { 99 | call.receive() 100 | } catch (e: BadRequestException) { 101 | respondError( 102 | HttpStatusCode.BadRequest, 103 | 400, 104 | "midercode is required: ${e.message}", 105 | logger, 106 | e 107 | ) 108 | null 109 | } 110 | } 111 | 112 | // 统一处理 API 请求 113 | private suspend fun PipelineContext.handleApiRequest( 114 | config: Configuration, 115 | workspace: File, 116 | logger: Logger, 117 | isDirectApi: Boolean 118 | ) { 119 | val parameter = receiveParameter(logger) ?: return 120 | _handleApiRequest(parameter, config, isDirectApi, logger, workspace) 121 | } 122 | 123 | private suspend fun PipelineContext._handleApiRequest( 124 | parameter: ServiceParameter, 125 | config: Configuration, 126 | isDirectApi: Boolean, 127 | logger: Logger, 128 | workspace: File 129 | ) { 130 | parameter.copy(config) 131 | 132 | try { 133 | val (result, streamList) = config.generate(parameter.midercode) 134 | 135 | if (isDirectApi) { 136 | handleDirectApiResponse(result, streamList, logger) 137 | } else { 138 | handleStandardApiResponse(result, streamList, parameter, workspace, logger) 139 | } 140 | } catch (e: Throwable) { 141 | respondError( 142 | HttpStatusCode.BadGateway, 143 | 500, 144 | "server error: ${e.message}", 145 | logger, 146 | e 147 | ) 148 | } 149 | } 150 | 151 | // 处理标准 API 响应 152 | private suspend fun PipelineContext.handleStandardApiResponse( 153 | result: ProduceCoreResult, 154 | streamList: List>, 155 | parameter: ServiceParameter, 156 | workspace: File, 157 | logger: Logger 158 | ) { 159 | val links = streamList.mapIndexed { index, (stream, name) -> 160 | val ext = determineFileExtension(name, result) 161 | val fileName = parameter.midercode.hash() 162 | val file = File(workspace, "$fileName-${index + 1}.$ext") 163 | 164 | if (!file.exists() || !parameter.cache) { 165 | file.writeBytes(withContext(Dispatchers.IO) { 166 | stream.readAllBytes() 167 | }) 168 | logger.info("Generated file at: ${file.absolutePath}") 169 | } 170 | 171 | mapOf(name to "/generated/${file.name}") 172 | } 173 | 174 | call.respond( 175 | ResponseBody( 176 | stateCode = 200, 177 | state = "success", 178 | message = "Successfully generated midercode", 179 | type = determineResponseType(result), 180 | link = links 181 | ) 182 | ) 183 | } 184 | 185 | // 处理直接 API 响应 186 | private suspend fun PipelineContext.handleDirectApiResponse( 187 | result: ProduceCoreResult, 188 | streamList: List>, 189 | logger: Logger 190 | ) { 191 | val (stream, name) = streamList.first() 192 | val contentType = determineContentType(name, result) 193 | 194 | call.respondBytes(contentType = contentType) { 195 | withContext(Dispatchers.IO) { 196 | stream.readAllBytes() 197 | } 198 | } 199 | } 200 | 201 | // 处理文件请求 202 | private suspend fun PipelineContext.handleFileRequest( 203 | workspace: File, 204 | logger: Logger 205 | ) { 206 | val hash = call.parameters["hash"] 207 | 208 | if (hash.isNullOrBlank()) { 209 | respondError( 210 | HttpStatusCode.BadRequest, 211 | 400, 212 | "filename is required", 213 | logger 214 | ) 215 | return 216 | } 217 | 218 | try { 219 | call.respondFile(workspace, hash) 220 | } catch (e: Throwable) { 221 | respondError( 222 | HttpStatusCode.BadGateway, 223 | 500, 224 | "server error: ${e.message}", 225 | logger, 226 | e 227 | ) 228 | } 229 | } 230 | 231 | // 确定文件扩展名 232 | private fun determineFileExtension(name: String, result: ProduceCoreResult): String { 233 | return when { 234 | '.' in name -> name.substringAfterLast('.') 235 | else -> when { 236 | result.isUploadMidi -> "mid" 237 | result.isRenderingNotation -> "tmp" 238 | else -> "mp3" 239 | } 240 | } 241 | } 242 | 243 | // 确定响应类型 244 | private fun determineResponseType(result: ProduceCoreResult): String { 245 | return when { 246 | result.isSing -> "sing" 247 | result.isUploadMidi -> "midi" 248 | result.isRenderingNotation -> "notation" 249 | else -> "mp3" 250 | } 251 | } 252 | 253 | // 确定内容类型 254 | private fun determineContentType(name: String, result: ProduceCoreResult): ContentType { 255 | val extension = if ('.' in name) name.substringAfterLast('.') else null 256 | 257 | return when (extension) { 258 | "png" -> ContentType.Image.PNG 259 | "mp3" -> ContentType.Audio.MPEG 260 | "mid", "silk" -> ContentType.Audio.Any 261 | null -> when { 262 | result.isUploadMidi -> ContentType.Audio.Any 263 | result.isRenderingNotation -> ContentType.Any 264 | else -> ContentType.Audio.MPEG 265 | } 266 | else -> ContentType.Any 267 | } 268 | } 269 | 270 | // 处理 /m 和 /md 接口的请求 271 | private suspend fun PipelineContext.handleMidercodeRequest( 272 | config: Configuration, 273 | workspace: File, 274 | logger: Logger, 275 | defaultRaw: Boolean 276 | ) { 277 | // 获取查询参数 278 | val cParam = call.parameters["c"] 279 | val bParam = call.parameters["b"] 280 | val rawParam = call.parameters["raw"] 281 | 282 | // 解析 midercode 283 | val midercode = when { 284 | !cParam.isNullOrBlank() -> { 285 | // c 参数存在,使用 URL 解码 286 | try { 287 | URLDecoder.decode(cParam, "UTF-8") 288 | } catch (e: Exception) { 289 | respondError( 290 | HttpStatusCode.BadRequest, 291 | 400, 292 | "Invalid URL encoding in parameter 'c': ${e.message}", 293 | logger, 294 | e 295 | ) 296 | return 297 | } 298 | } 299 | 300 | !bParam.isNullOrBlank() -> { 301 | // b 参数存在,使用 Base64 解码 302 | try { 303 | String(Base64.getDecoder().decode(bParam), Charsets.UTF_8) 304 | } catch (e: Exception) { 305 | respondError( 306 | HttpStatusCode.BadRequest, 307 | 400, 308 | "Invalid Base64 encoding in parameter 'b': ${e.message}", 309 | logger, 310 | e 311 | ) 312 | return 313 | } 314 | } 315 | 316 | else -> { 317 | respondError( 318 | HttpStatusCode.BadRequest, 319 | 400, 320 | "Either parameter 'c' or 'b' is required", 321 | logger 322 | ) 323 | return 324 | } 325 | } 326 | 327 | // 解析 raw 参数 328 | val isDirectApi = when { 329 | rawParam != null -> rawParam.toBoolean() 330 | else -> defaultRaw 331 | } 332 | 333 | // 创建 ServiceParameter 334 | val parameter = ServiceParameter(midercode = midercode) 335 | 336 | _handleApiRequest(parameter, config, isDirectApi, logger, workspace) 337 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MidiProduce 2 | 3 | ![](https://img.shields.io/github/downloads/whiterasbk/MiraiMidiProduce/total) 4 | ![](https://img.shields.io/github/v/release/whiterasbk/MiraiMidiProduce?display_name=tag) 5 | ![](https://img.shields.io/github/languages/top/whiterasbk/MiraiMidiProduce) 6 | ![GitHub](https://img.shields.io/github/license/whiterasbk/MiraiMIdiProduce) 7 | 8 | 在线作曲插件 9 | 10 | ## 安装方法 11 | 下载见 `release` 下的 `bot-version.jar` 12 | 1. 打开 `plugins` 文件夹 13 | 2. 丢进去 14 | 3. 关闭文件夹 15 | 16 | ## 效果 17 | ![你看到了一张图](https://mirai.mamoe.net/assets/uploads/files/1653475903211-d7c4283d-8163-4079-8c60-11d26468d8b1-image.png) 18 | [BV1gS4y1W7cj](https://www.bilibili.com/video/BV1gS4y1W7cj) 19 | 20 | ## 使用方法 21 | 22 | ### 普通指令(轨道语法) 23 | 24 | `>params>sequence` 25 | 26 | 27 | 在 mider code 中, `>params>sequence` 可以称作一条轨道 28 | #### params 29 | 参数之间用`;`号分隔 30 | 支持的参数 `params` 详细说明如下: 31 | 32 | 1. `g|f|数字+b` 设置 bpm 同时确定音域, f 表示低音 (pitch=3&bpm=80), g 表示高音 (pitch=4&bpm=80) **必选参数不可省略**. 示例: 33 | ```shell 34 | >g> 35 | >f> 36 | >120b> 37 | ``` 38 | **所有轨道共用一个 bpm** 39 | 40 | 2. `[数字+x]` 设置倍速, 支持整数和小数 41 | 3. `[/+数字]` 以设定值为音符默认时值 42 | 4. `[数字+%]` 设置轨道音量 43 | 5. `[数字+dB]` 以设定值为音符默认音量, 范围 0~127 44 | 6. `[调号]` 默认当前音符序列为 C大调 并将其转调到指定调号. 调号由两部分组成: 一部分是调的名字, 具体参考乐理, 需要大写, 另一部分是调的模式, major 和 minor 分别可以简写成 maj 和 min. 若为 minor 则为同名小调. 在一般情况下 major 可以省略. 示例: 45 | ```text 46 | >g;Cmin> 47 | >g;#Fmajor> 48 | >g;bD> 49 | ``` 50 | 7. `[数字]` 设置默认音高 51 | 8. `[数字/数字]` 设置拍号 52 | 9. `[i=instrument]` 设置乐器, `instrument` 见 [MidiInstrument.kt](https://github.com/whiterasbk/mider/blob/dev/src/main/kotlin/whiter/music/mider/MidiInstrument.kt) 53 | 10. `[midi|img|pdf|mscz]` 上传乐谱文件, 如是 img 则会上传图片 **注: 涉及乐谱生成需要先安装 Muse Score** 见 [转换乐谱](#转换乐谱) 54 | 11. `[sing:area:singerId]` 调用 [sinsy](https://www.sinsy.jp/) 接口生成音频. 现在只支持 单个音符 和 休止符. 当歌词包含英文时必须使用音名序列. 当 area 和 singerId 都提供时, singerId的格式为 [f|m]+数字 表示选取的是该地区的 第 singerId 位 女性|男性 歌手. 当仅提供 singerId 时, 其格式为 数字, 表示选取 表示符 为 singerId 的歌手. 当两者都不提供时, 相当于选取中国大陆地区的第一位女歌手: 香玲. area 的取值目前只有 cn|us|jp 55 | 56 | #### sequence 57 | sequence 可以是音名序列或是唱名序列, 音名序列的判断标准是序列里是否出现了 `c~a` 或 `C~B` 中任何一个字符 58 | ##### 音符 59 | 音名序列下, 使用 cdefgabCDEFGAB 表示, 其中小写写表示 pitch=4的音符, 大写表示比小写高一个八度的音符 60 | 61 | 唱名序列下, 使用 1234567 表示 62 | 63 | 创建的音符默认时值是四分音符, 要修改其时值, 可以使用 `+` 来拉长音符时值, `-` 以缩短时值 64 | 例如以下示例就创建了一个八分音符, 十六分音符和一个二分音符 65 | ```shell 66 | >g>c- #八分音符 67 | >g>c-- #十六分音符 68 | >g>c+ #二分音符 69 | ``` 70 | 要为音符添加附点, 请在该音符后加上 `.`, 而特殊时值例如三连音可以使用 `/` 71 | 72 | `/` 也可以不跟着 `3`, 实际上 `1~9` 都是可以的; 同时也提供了 `x` 符号表示当前音符时值的倍数关系, 用法完全和 `/` 一样 73 | 74 | 除了 `/3` 以外, 其他特殊时值很少使用, 并且在转换其他格式时容易因精度而出现问题, 除非你确实要这个效果, 否则**应尽量避免这类用法** 75 | ```shell 76 | >g>a. c/3 c/3 c/3 c/5 cx5 77 | ``` 78 | 79 | `#` 和 `$` 分别可以让音符升高或降低半音, `@` 表示给音符添加一个还原符号 80 | ```text 81 | >g>$e @f #c 82 | ``` 83 | 如果不嫌麻烦, Unicode中的 `♯`, `♮` 和 `♭` 同样可以起到相同作用 84 | ```shell 85 | >g>♭e ♮f ♯c 86 | ``` 87 | `&` 可以将多个音符的时值连在一起组成一个音符 88 | ```shell 89 | >g>a&a- # 等价于以下 90 | >g>a. 91 | ``` 92 | 而在唱名序列中, `$` 可以用 `b` 代替 93 | ```shell 94 | >g>12b3 95 | ``` 96 | 在音符后加上数字可以改变其八度 97 | ```shell 98 | >g>c3 e5 99 | ``` 100 | 而更推荐的做法是使用 `↑` 和 `↓` 在 pitch=4 的基础上进行八度的增减. 而在唱名序列中, `↑` 和 `↓` 可以用 `i` 和 `!` 代替 101 | ```shell 102 | >g>c↓ e↑ g↑↑ 103 | >g>1↓ 3↑ 5↑↑ 104 | >g>1! 3i 5ii 105 | ``` 106 | `%` 可以设置单个音符的力度, 范围是 0~127, 未设置的情况下默认力度为 100 107 | ```shell 108 | >g>c%60 109 | ``` 110 | 如果需要单独设置音符按下和松开时的力度, 只需要在 `%` 后加上 `↓` 和 `↑` 111 | ```shell 112 | >g>c%↓60 c%↑70 113 | ``` 114 | 使用 `o` 或 `O` 创建四分休止符或二分休止符, 休止符同样可以使用 `+`, `-` `.` 甚至 `/` 来修改时值. 在 唱名序列中 `0` 相当于 四分休止符 115 | ```shell 116 | >g>o-O+O. 117 | ``` 118 | `[` 配合 `]` 可以给单个音符加上歌词 119 | ```shell 120 | >g>1[打]2[倒]3[列]1[强] 121 | ``` 122 | `[]` 内使用单个空格分割可以给多个音符添加歌词; 同时, `_` 可以作为占位符 123 | ```shell 124 | >g>1231 [打 倒 列 强] 125 | ``` 126 | 要重复单个音符多次可以使用 `*` 127 | ```shell 128 | >g>c*100 129 | ``` 130 | `~` 可以克隆前一个音符, ~~适合偷懒~~ 131 | ```shell 132 | >g>c~~~~~~ 133 | ``` 134 | `^` 和 `v` 可以将上一个音符克隆并升高或降低一个音, 升高或降低的音满足在 C大调 下的音程关系. 类似的用法还有 `m-w`, `n-u`, `q-p`, `i-!`, `s-z` 升高或降低度数在 ^-v 的基础上逐步递增或递减 135 | ```shell 136 | >g>c^^^ # 等价于 cdef 137 | ``` 138 | 139 | ##### 和弦 140 | 使用 `:` 可以将多个音符组成一个和弦, 第一个音的时值将会是和弦的时值 141 | ```shell 142 | >g>c:e:g 143 | ``` 144 | `^` 和 `v` 等 也是可用的 145 | ```shell 146 | >g>c:m:m # 等价于 c:e:g 147 | ``` 148 | 但是 使用 `^` 和 `v` 等 时要注意, `#` 和 `$(b)` 将不起作用 149 | ```shell 150 | >g>c:m:#m #号 将不起作用 151 | ``` 152 | 可以使用 `"` 和 `'` 代替 `#` 和 `$(b)` ~~问就是起名废~~ 153 | ```shell 154 | >g>c:m:m' 155 | ``` 156 | `↟` 和 `↡` 可以创建向上或向下琶音 ~~符号越来越奇怪了啊喂~~ 157 | ```shell 158 | >g>c:e:g↟ 159 | ``` 160 | `t` 可以使 和弦中的音符的时值可以独立作用, 此时和弦的时值是组成音符中时值最长的那个 161 | ```shell 162 | >g>c+:e:g-t 163 | ``` 164 | 165 | ##### 倚音 166 | `;` 连接两个音符组成一个短前倚音, 倚音时值为第二个音符的时值 167 | ```shell 168 | >g>c;e 169 | ``` 170 | 若要构建后倚音只需要在第二个音符后加上 `t` 171 | ```shell 172 | >g>c;et 173 | ``` 174 | 175 | ##### 滑音/刮奏(Glissando) 176 | 使用 `=`, 可以连接多个音符, 时值为所有组成音符的时值. 默认只刮白键 177 | ```shell 178 | >g>c=b 179 | ``` 180 | 若要白键和黑键一起刮, 在后面加上 `t` 181 | ```shell 182 | >g>c=bt 183 | ``` 184 | 185 | ##### 宏 186 | mider code 中宏的本质是对某段序列或其中的字母或数字的重复或简单修改替换. 187 | 188 | 碍于技术原因, 目前宏均不可嵌套使用 189 | 190 | 宏的定义始于 `(` 终于 `)`, `()` 内便是宏的作用域, 以下是支持的宏: 191 | 192 | 定义一个音符序列: `(def symbol=note sequence)` 193 | ```shell 194 | >g>aaa(def na=cde)aaa 195 | >g>aaaaaa # 实际输入的序列 196 | ``` 197 | 定义一个音符序列, 并在此处展开: `(def symbol:note sequence)` 198 | ```shell 199 | >g>aaa(def na=cde)aaa 200 | >g>aaacdeaaa # 实际输入的序列 201 | ``` 202 | 展开 symbol 对应音符序列: `(=symbol)` 203 | ```shell 204 | >g>(def a=cde)a(=a) 205 | >g>acde # 实际输入的序列 206 | ``` 207 | 读取 path 代表的资源并展开, 如果是文件默认目录是插件的数据文件夹: `(include path)` 208 | ```shell 209 | >g>(include ./seq.midercode) 210 | ``` 211 | 将音符序列重复 times 次: `(repeat time: note sequence)` 212 | ```shell 213 | >g>c(repeat 3: oa) 214 | >g>coaoaoa # 实际输入的序列 215 | ``` 216 | 如果定义了 symbol 则展开: `(ifdef symbol: note sequence)` 217 | 218 | 如果未定义 symbol 则展开: `(if!def symbol: note sequence)` 219 | ```shell 220 | >g>(def s=abc) (ifdef s: cfg) (if!def s: bbc) 221 | >g>cfg #实际输入的序列 222 | ``` 223 | 定义宏(类似函数, 但实际表现得更蠢一些): `(macro name param1[,params]: note sequence @[param1])` 224 | 225 | 展开宏: (!name arg1[,arg2]) 226 | ```shell 227 | >g>(m p1: a@[p1]dc@[p1]) (!m b) 228 | >g>abdcb #实际输入的序列 229 | ``` 230 | 调整 note sequence 的力度, 仅适用于长音名序列: `(velocity linear from~to: note sequence)` 231 | ```shell 232 | >g>(velocity linear 50~80: cde) 233 | >g>c%50 d%60 e%70 234 | ``` 235 | 注释: `(# comment)` 236 | ```text 237 | >g>(# 这是一段注释) 238 | ``` 239 | 240 | ### 环境指令: `>!config>` 241 | 供 MidiProduce 内部调用 242 | 获取帮助 243 | ```shell 244 | >!help 245 | ``` 246 | 设置 formatMode 247 | ```shell 248 | >!formatMode=mode 249 | ``` 250 | 清理缓存 251 | ```shell 252 | >!clear-cache 253 | ``` 254 | 发送来自 `awesome-melody` 的 示例 `midercode` 255 | ```shell 256 | >!sample 257 | ``` 258 | 259 | ## todo list 260 | 261 | - [x] 解析音符为语音 262 | - [x] 渲染乐谱 263 | - [ ] 识别乐谱并转化为音符 264 | 265 | 266 | ## 示例 267 | ``` 268 | 1. 小星星 269 | >g>1155665 4433221 5544332 5544332 270 | 等同于 271 | >g>ccggaag+ffeeddc+ggffeed+ggffeed 272 | 等同于 273 | >g>c~g~^~v+f~v~v~v+(repeat 2:g~v~v~v+) (酌情使用 274 | 275 | 2. KFC 可达鸭 276 | >g;bE>g^m+C-wmD+D^m+G-wmE+D^w+C-wmD+DvagaC 277 | 278 | 3. 碎月 279 | >85b>F+^$BC6GFG C$E F D$ED$b C+ g$b C$E F$E F+ F$E F$B G++ G$B C6C6$B C6 G+ G$E FGF$E C+ C$b C+C$EF$EFG $E 280 | 等同于 281 | >85b;Cmin>F+^BC6GFG CE F DEDb C+ gb CE FE F+ FE FB G++ GB C6C6B C6 G+ GE FGFE C+ Cb C+CEFEFG E 282 | 283 | 4. 生日快乐 284 | >88b>d.d- e+v g+ #f++ d.d- e+v a+ v+ d.d- D+b+g+ #f+ e+ C.C- b+ g+^ v+ 285 | 286 | 5. 茉莉花 287 | >110b>e+em^m~wv+g^v++e+em^m~wv+g^v++g+~~em^+av~++e+d^m+evv+c^v++evvmv+.eg+amg++d+egd^cwv++ ^-c+d+.ec^vwv++ 288 | 289 | 6. bad apple! 290 | >100b>e#fgab+ ED b+ e+ b a-- B-- A- g#f e#fga b+ ag #fe#fg #f--G--#F-e #d#f e#fgab+ ED b+e+ ba--B--A- g#f e#fgab+ ag 291 | 292 | 7. Jingle Bells 293 | >100b>E~~+E~~+EmC^^++F~~+Fv~+Ev~^ D+G+E~~+E~~+EmC^^++F~~+Fv~~m~vDv++ 294 | 295 | 8. 两只老虎 卡农 296 | >g;3>(def tiger:1231 1231 3450 3450 5-6-5-4-31 5-6-5-4-31 15!10 15!10) 297 | >g;4>00(=tiger) 298 | >g;5>0000(=tiger) 299 | >g;6>000000(=tiger) 300 | >g;7>00000000(=tiger) 301 | ``` 302 | 更多示例见 [awesome-melody](https://github.com/whiterasbk/MiraiMidiProduce/tree/master/awesome-melody) 303 | 304 | 若想分享自己编写的旋律欢迎提 `pr` 到这个文件夹, **建议使用英文名称**, 后续可能会考虑打包进发布版本供 `include` 使用 305 | 306 | ## 转换乐谱 307 | **此功能需要首先安装 [Muse Score](https://musescore.org/zh-hans)** 308 | 309 | 下载 `Muse Score` : 可以根据官方 [下载页面](https://musescore.org/zh-hans/download) 也可以参考 [snapcraft](https://snapcraft.io/musescore) 310 | 311 | 附官方 `linux` [安装指北](https://musescore.org/zh-hans/handbook/3/install-linux) 312 | 313 | 安装完成后将 `Muse Score` 的运行目录 (包括`bin/`) 添加到环境变量 314 | 315 | 或者也可以修改配置中`mscoreConvertMidi2MSCZCommand` 等的值为安装目录 316 | 317 | #### 如您的安装的可执行程序启动命令(可执行程序的名字)不是 `MuseScore3`, 您需要手动将 `config.yml` 中的 `MuseScore3` **替换**成正确的 `MuseScore` 启动命令 318 | 319 | 最后在轨道中添加 `;pdf` 或 `;img` 即可得到渲染好的乐谱 320 | 321 | ![44f9b717-4c28-453e-b99c-2fc8567828c8-image.png](https://mirai.mamoe.net/assets/uploads/files/1654083503837-44f9b717-4c28-453e-b99c-2fc8567828c8-image.png) 322 | 323 | 若想修改 `Muse Score` 命令格式和参数, 请参考 [官方使用手册](https://musescore.org/zh-hans/handbook) 324 | 325 | ## 注意 326 | - 唱名序列中 `\s{2}` 和 `\s\|\s` 会被自动替换成 `0` 也就是休止符, 可以在配置中修改这部分行为 327 | - 若为使用 `internal` 模式则生成的语音音色会随着系统底层实现的不同而不同 328 | - 如果最后输出的格式是 `silk` 那么好友和群聊都有效, 如果是 `mp3` 则仅群聊有效, 好友会出现感叹号 329 | - `mp3` 格式在 `pc` 端听不了, `mac` 据说可以 ~~, 哪位富婆可以给咱买一台测试一下(~~ 330 | - 命令还未加入权限, 可以在 [#3](https://github.com/whiterasbk/MiraiMidiProduce/issues/3) 进行讨论 331 | - 好友环境下生成 `silk` 格式会比 `mp3` 音质低得多 ~~, 听个响属于是~~ 332 | - 当文本过于长超过 QQ 消息的限制时, 可以将 midercode 保存到文本文件中并修改其扩展名为 .midercode, 上传后 机器人同样能正常识别 333 | 334 | ## 构建 335 | 336 | 由于使用了 [RainChan](https://github.com/mzdluo123) 的 [silk4j](https://github.com/mzdluo123/silk4j) 所以 `clone` 到本地后要修改 `build.gradle.kts` 中的 `username` 和 `password` 为自己的才能成功构建 337 | 338 | ## 服务器环境下生成语音 339 | 340 | 在服务器环境插件可能会由于缺少硬件或驱动支持无法生成语音, 可以尝试安装 [timidity](http://timidity.sourceforge.net/) 和 [ffmpeg](http://ffmpeg.org/) 解决 341 | 342 | 具体安装可以参考 [这篇](https://www.cnblogs.com/koujiaonuhan/p/aliyun_centos65_install_ffmpeg_libmp3lame_timidity_to_convert_midi_to_mp3.html) 343 | 344 | 这里提供一个 `sf2` 的 [音色库](https://cowtransfer.com/s/2f42efd92be448) 345 | 346 | 安装完成以后确保 `timidity` 和 `ffmpeg` 位于环境变量中, 或者也可以修改 `ffmpegConvertCommand` 和 `timidityConvertCommand` 347 | 348 | 最后修改 `formatMode` 即可使用 `timidity` 和 `ffmpeg` 生成语音 349 | 350 | ## 修改音色 351 | 352 | 目前 `internal` 无法修改音色 353 | 354 | 可以通过安装 `timidity` 或 `Muse Score` 来实现 355 | 356 | ## 命令行工具 357 | `mider produce` 工程提供了一个简单易用的命令行工具以在 `mirai` 环境之外使用 `midercode`, 下载地址位于 `release` 下的 `cl-version.jar` 358 | 359 | 运行此工具需要的配置: 360 | - jdk11 或更高 361 | 362 | #### 使用方法 363 | 364 | 1. 交互模式 365 | ```bash 366 | java -jar cl-version.jar 367 | ``` 368 | 进入交互模式 369 | ```bash 370 | >g> 371 | ``` 372 | 此时输入 midercode 即可实时解析并播放 373 | ```bash 374 | >g> aaaaa 375 | >g> 123 376 | ``` 377 | 要更换轨道设置, 只需要输入 `>settings>` 即可切换轨道设置 378 | ```bash 379 | >g> >g;4x> 380 | >g;4x> 1 381 | ``` 382 | 2. 播放 `midercode` 383 | ```bash 384 | java -jar cl-version.jar file.midercode 385 | ``` 386 | 3. 转换 `midercode`, 目前只支持 `mp3` 格式 387 | ```bash 388 | java -jar cl-cersion.jar file.midercode -o file.mp3 389 | ``` 390 | 访问 `sinsy` 时可以指定 `-proxy` 参数以增快访问速度 391 | 392 | ## 服务端 393 | `release` 下的 `server-version.jar` 是服务端程序, 通过 `java -jar server-version.jar` 启动以后可以通过 `http` 接口访问 生成 `midercode` 的功能 394 | 395 | 以下是接口使用方式 396 | 397 | ```text 398 | POST localhost:8080/api 399 | Content-Type: application/json 400 | 401 | { 402 | "midercode": ">g>1155665 4433221" 403 | } 404 | ``` 405 | 返回 406 | ```json 407 | { 408 | "status": "success", 409 | "type": "mp3", 410 | "links": [ 411 | { 412 | "stream": "/generated/3680d25dc5832bd2652a366c6cb23e3a7135ba5932b5c98335a5ec77b9342f29-1.mp3" 413 | } 414 | ] 415 | } 416 | ``` 417 | 其中 `stream` 即是生成的链接, 再通过 get 请求该链接即可得到流 418 | ```text 419 | GET localhost:8080/generated/3680d25dc5832bd2652a366c6cb23e3a7135ba5932b5c98335a5ec77b9342f29-1.mp3 420 | ``` 421 | 422 | ## release 中的多个发行包 423 | 424 | 带 `bundled-silkf4` 的是打包了 [silk4j](https://github.com/mzdluo123/silk4j) 的包 425 | 426 | 若确定不需要使用转换 `silk` 的功能可以直接下载不带后缀版本的包 427 | -------------------------------------------------------------------------------- /service/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Mider Code - 音乐代码生成器 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 43 | 44 | 233 | 234 | 235 | 236 |
237 | 238 | 239 |
240 |

241 | 🎵 Mider Code 242 |

243 |

将音符代码转换为美妙的音乐

244 |
245 | 246 | 247 |
248 | 249 | 250 |
251 | 254 | 263 | 264 |
265 | 279 | 291 | 297 |
298 |
299 | 300 | 301 |
302 |

❌ 错误

303 |

{{ errorMessage }}

304 |
305 | 306 | 307 |
308 |

✅ 成功

309 |

{{ successMessage }}

310 |
311 | 312 | 313 |
369 | 370 | 371 |
372 |
373 |
374 |
375 |

🎹 MIDI 文件

376 |

可在 DAW 或音乐软件中编辑

377 |
378 | 383 | 📥 下载 MIDI 384 | 385 |
386 |
387 |
388 | 389 | 390 |
391 |
392 |

393 | 🎼 生成的乐谱 394 |

395 |
396 | 402 | 408 |
409 |
410 | 411 | 412 | 432 | 433 | 434 |
435 |
440 |
441 | 第 {{ index + 1 }} 页 442 | 447 | 📥 下载 448 | 449 |
450 |
451 | 458 |
459 |
460 |
461 |
462 | 463 | 464 |
465 |
🎵
466 |

输入 Mider Code 开始创作音乐

467 |

支持生成 MP3、MIDI 和乐谱

468 |
469 |
470 | 471 | 472 |
473 |

📖 使用说明

474 |
475 |
476 |

🎹 基础音符

477 |

使用字母表示音符:c d e f g a b

478 |

例如: cdefgab

479 |
480 |
481 |

⏱️ 节奏控制

482 |

使用数字表示时值:1 2 4 8

483 |

例如: c1d2e4

484 |
485 |
486 |

🎚️ 音高调整

487 |

使用 > 和 < 调整八度

488 |

例如: >c<g

489 |
490 |
491 |
492 | 493 | 494 |
499 |
500 |
501 | 508 | 515 | 521 |
522 |
523 | 528 |
529 |
530 | 531 | {{ currentNotationIndex + 1 }} / {{ notationImages.length }} 532 | 533 |
534 |
535 |
536 |
537 | 538 | 816 | 817 | 818 | 819 | 820 | 840 | 841 | 842 | 843 | --------------------------------------------------------------------------------