Vmse plays music through Simple Voice Chat's positional audio channels, so the sound really does come from a place in the world: two sources at ±45° that turn with your head, a proper binaural stage, and a filter that keeps other players from hearing what is meant for you.
It also does the boring parts: playlists, music regions, downloads from a URL or a Bilibili page, and lyrics that stay in step with the audio.
Spatial stereo Two sources placed either side of the listener with inter-aural delay, head shadow and crossfeed. The image stays put when you turn, because the sources are recomputed from your yaw every tick — with velocity prediction so it does not lag at sprint speed.
mono / stereo / wide, chosen per player with /vmse surround.
5.1 was removed in 3.0. Six streams cost three times the bandwidth and, in a game where everyone wears headphones, sounded worse than two well-processed sources. The binaural renderer does the work instead.
Low bandwidth by default
A player who has never run /vmse surround gets one mono stream on a static
channel: half the packets, no position updates at all, and no chance of drift.
Running /vmse surround stereo opts that player into the spatial path for good.
Eight effect presets
off, studio, headphone, wide, hall, bass, vocal, night — real
DSP: biquad EQ, stereo width, crossfeed, early reflections, reverb, compressor
and a limiter that actually holds. viper, dolby, 3d and a few other names
are accepted as aliases.
Music regions Circle or square zones that start a playlist when someone walks in and stop when they leave. Region playback has its own volume, layout and effect.
Playlists Sequential, shuffle, loop, loop-one. Shuffle uses a bag, so a twenty-song playlist really plays twenty different songs before repeating one. Stored as YAML, per player or server-wide.
Downloads
/vmse download <link> <name> handles a direct audio URL, or — with yt-dlp
configured — a Bilibili, YouTube or SoundCloud page. The response's
Content-Type decides the file extension, so a .mp3 link that actually serves
Opus is stored correctly instead of producing a file nothing can decode.
Private addresses, file:// URLs and oversized downloads are refused.
Downloaded packs (.zip)
A download link can point at a zip containing tracks and a lyric sheet. The archive is treated as hostile from the first byte:
- entry names may not contain
.., may not be absolute, and may not use backslashes or drive letters - only audio and lyric extensions are allowed; anything else rejects the pack
- the first bytes of every entry are checked:
MZ,ELF, Mach-O, Java class and#!signatures reject the archive - limits on entry count, per-entry size, total size and compression ratio stop zip bombs
- the declared size is not trusted; bytes are counted while writing
Anything that fails is deleted along with its staging directory. A pack that
passes is unpacked into the cache, the archive is deleted, same-named files are
replaced, and playback starts immediately. /vmse download puts the tracks in
the music folder with the lyric beside them instead.
Built-in song recognition
/vmse index rebuild fingerprints every track in the music folder; the index
lives in plugins/Vmse/fingerprints.bin and nothing is uploaded anywhere.
/vmse identify <file> then answers "which of my songs is this, and how far
in?" - a 20 second clip cut from 60 s into a track comes back as that track at
60.0 s with 94% confidence.
The fingerprint is the classic spectral-peak-pair design: find the strongest peaks, hash each pair together with the gap between them, and let thousands of matching hashes vote on one offset. It survives re-encoding, volume changes and noise because it never compares waveforms.
/vmse lyrics <file> uses it: if the file is a recording of something already
in the library, that song's lyric timeline is reused and moved to the right
place instead of being guessed from energy. Only when nothing matches does it
fall back to segmentation.
A song with no lyric file at all gets one the first time it is played: the timeline is written next to the music and reused from then on. An existing lyric file is never overwritten - a hand-made LRC or JSON wins, because the JSON takes priority when displaying and a generated placeholder would silently replace the real words.
Lyrics come from the network, not from guessing
There is no "make up a timeline from the waveform" mode any more. That path was removed: measured against a real 53-line sheet it landed 3.5 seconds out on average and 9.4 seconds out at worst, which is a wrong lyric on screen, not a feature. A wrong line is worse than no line.
What happens instead, in order:
- A lyric file next to the track, or in
plugins/Vmse/lyrics/, is used as is. - Otherwise the track is looked up on LRCLIB and NetEase, in that order.
- Every candidate is scored: title 55%, artist 25%, length 20%. A search result that merely shares a title scores low and is thrown away.
- The sheet itself has to fit the file: at least four lines, monotonic timings, nothing running more than eight seconds past the end, and not stopping in the first third. A live version, a cover or a remix fails that check.
- Only an accepted sheet is written, as
mc_lyricsJSON, and it is reused from then on.
Tested on 反乌托邦 by 乌托邦P with no local lyric file: found on the network, 53 lines, first at 0 ms, "反乌托邦中的争夺" at 15 900 ms, "在这反乌托邦里" at 129 870 ms - identical to the hand-made LRC. A same-titled decoy by a different artist was ranked below the threshold and rejected.
Lyrics
mc_lyrics v1 JSON (one start_ms / end_ms per line, gaps hide the line),
plain LRC, or generated on the spot. Displayed on the action bar, title, boss
bar or chat, with optional per-word highlighting.
Selectors that behave
me, all, @a, @s, @p, @r, @e and player names, all resolved the way
vanilla does. all is a selector, not a player called "all".
Permissions you can actually edit
plugins/Vmse/permissions.yml declares every sub-command as admin, player
or disabled. /vmse perms shows the effective table, /vmse reload perms
applies it without a restart.
- Install Simple Voice Chat — it is required, Vmse uses its audio API.
- Drop
VMSE-1.2.0-1.19-1.21.11.jarintoplugins/. - Start the server.
config.yml,permissions.ymland the language files are written on first run. - Put music in
plugins/Vmse/music/— decoded natively, no external tools: mp3, ogg, wav, flac, aiff, au and raw PCM. FLAC uses a built-in decoder that is bit-exact against the reference encoder. Addffmpegfor m4a/alac, aac, wma, ape, wavpack, opus, dsd and amr. /vmse help.
Optional but recommended: set network.yt-dlp and network.ffmpeg in the
config if you want page links and the wider format list.
| Command | What it does |
|---|---|
/vmse play [target] <song> [-original] |
Play a file; -original bypasses every effect and cache |
/vmse url <target> <link> |
Play a link without saving it |
/vmse stop / pause [target] |
Stop or pause |
/vmse list [page] |
Browse the music folder |
/vmse effect <list|set|info> [preset] [target] |
Effect presets |
/vmse surround [target] [mono|stereo|wide] |
Choose a layout |
/vmse playlist <create|delete|add|remove|mode|play|stop|list> |
Playlists |
/vmse region <create|remove|list|info|bind|track|volume|layout> |
Music regions |
/vmse wand |
Get the wand (region and source selection) |
/vmse download <link> <name> |
Download audio |
/vmse lyrics <song> |
Generate a lyric timeline |
/vmse identify <file> |
Work out which song a recording is |
/vmse index <status|list|rebuild> |
Build or inspect the fingerprint index |
/vmse vmsg <song> |
Send as a voice message |
/vmse confirm [cancel] |
Confirm a large broadcast |
/vmse stats |
Engine state: frames, late packets, cache |
/vmse perms |
Show the permission table |
/vmse reload <all|config|lang|perms> |
Reload |
Targets accept me, all, @a, @s, @p, @r or a player name. Leaving the
target out means yourself.
/vmse wand gives you a golden hoe. Left-click air to switch mode.
- Region mode — left-click one corner, right-click the other. A confirmation prompt shows the area and the estimated number of affected chunks.
- Source mode — right-click a block inside a region to place a sound source.
The interesting parts of config.yml:
playback:
low-bandwidth-default: true # mono static channel until /vmse surround
pcm-cache-max-mb: 192 # decoded audio kept in memory
surround:
default-layout: stereo
stereo-separation: 1.2 # half the distance between sources, in blocks
position-lead-ms: 60 # velocity prediction, fixes sprint lag
position-epsilon: 0.06 # skip updates smaller than this
network:
yt-dlp: '' # path to yt-dlp, empty disables page links
ffmpeg: '' # path to ffmpeg, empty disables extra formats
max-download-mb: 80
lyrics:
lookup-online: true # try LRCLIB when there is no sidecar file
auto-generate: true # build a timeline after /vmse downloadmvn -Pmc1194 clean package # 1.19 - 1.21.11 -> target/VMSE-1.2.0-1.19-1.21.11.jar
mvn -Pmc1165 clean package # 1.16.5 - 1.18.2 -> target/VMSE-1.2.0-1.16.5-1.18.2.jar
mvn -Pmc1122 clean package # 1.12 - 1.12.2 -> target/VMSE-1.2.0-1.12.2.jar
mvn -Pmc263 clean package # 26.1 - 26.3 -> target/VMSE-1.2.0-26.1-26.3.jar| Jar | Server | Java |
|---|---|---|
VMSE-1.2.0-1.12.2.jar |
1.12 - 1.12.2 | 8 |
VMSE-1.2.0-1.16.5-1.18.2.jar |
1.16.5 - 1.18.2 | 8 - 17 |
VMSE-1.2.0-1.19-1.21.11.jar |
1.19 - 1.21.11 | 17 - 21 |
VMSE-1.2.0-26.1-26.3.jar |
26.1 - 26.3 | 25 |
1.13 to 1.15 are skipped because Simple Voice Chat itself has no build for them: there is no way to play spatial audio there at all.
Each jar is compiled against that server's own API, so no jar relies on a method
that only exists in a newer version. The code base is Java 8 compatible throughout,
which is what makes the 1.12 jar possible: records, switch expressions and the
Java 9+ collection factories are gone, replaced by a small J8 helper class.
Where a version genuinely lacks an API, the call goes through guarded reflection
instead of a compile-time dependency: hex colours fall back to the closest vanilla
colour on 1.12-1.15, sendActionBar degrades to the chat bar, the wand marker uses
lore instead of a persistent data container, and the Voice Messages integration is
entirely reflective so 1.12 does not need that mod to exist at all.
All four jars were then started on a real server with Simple Voice Chat
2.6.27: 1.12.2 on Java 8, 1.16.5 on Java 11, 1.18.2 and 1.19.4 on Java 17,
1.20.6, 1.21.1 and 1.21.11 on Java 21, and 26.3 on Java 25. Each loaded the
plugin, registered with Simple Voice Chat and answered /vmse stats, with no
exceptions. Merging eight jars into four is justified by measurement, not by
hope: a jar built against the 1.19.4 API still loads on 26.3.
./test/run-tests.sh156 headless assertions over the parts that do not need a server: playlist modes and shuffle, LRC parsing, energy segmentation, lyric alignment, JSON output, audio resampling, WAV and raw PCM decoding, FLAC (bit-exact against a reference-encoded sample), fingerprint matching and index round-tripping, layout maths, effect presets and the downloader's URL guards. It runs on a phone.
AGPL-3.0. If you run a modified copy as a public service, publish the changes.