├── .editorconfig ├── .envrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── COPYING ├── Makefile ├── README.md ├── extensions.h ├── flake.lock ├── flake.nix ├── package.nix ├── tests ├── basic.nix └── test.adx └── vgm.c /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [Makefile] 8 | indent_style = tab 9 | 10 | [*.{c,h}] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: '30 13 * * *' 9 | 10 | jobs: 11 | check: 12 | runs-on: ubuntu-24.04 13 | permissions: 14 | id-token: "write" 15 | contents: "read" 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: DeterminateSystems/nix-installer-action@v17 19 | - uses: DeterminateSystems/magic-nix-cache-action@main 20 | with: 21 | source-url: https://github.com/jchw-forks/magic-nix-cache/releases/download/nightly/magic-nix-cache-X64-Linux 22 | - uses: DeterminateSystems/flake-checker-action@main 23 | with: 24 | ignore-missing-flake-lock: false 25 | fail-mode: true 26 | - name: Run nix flake checks 27 | run: nix flake check 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Make 2 | /vgm.so 3 | /compile_flags.txt 4 | *.o 5 | 6 | # Direnv 7 | /.direnv 8 | 9 | # Nix 10 | /result 11 | /result-* 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vgmstream"] 2 | path = vgmstream 3 | url = https://github.com/vgmstream/vgmstream 4 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | - deadbeef-vgmstream - 2 | Copyright (c) 2014 John Chadwick 3 | 4 | Licensed under the same terms as vgmstream (see below.) 5 | 6 | - vgmstream - 7 | Copyright (c) 2008-2010 Adam Gashlin, Fastelbja, Ronny Elfert 8 | Portions Copyright (c) 2004-2008, Marko Kreen 9 | Portions Copyright 2001-2007 jagarl / Kazunori Ueno 10 | Portions Copyright (c) 1998, Justin Frankel/Nullsoft Inc. 11 | Portions Copyright (C) 2006 Nullsoft, Inc. 12 | Portions Copyright (c) 2005-2007 Paul Hsieh 13 | Portions Public Domain originating with Sun Microsystems 14 | 15 | Permission to use, copy, modify, and distribute this software for any 16 | purpose with or without fee is hereby granted, provided that the above 17 | copyright notice and this permission notice appear in all copies. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 20 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 21 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 22 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 23 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 24 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 25 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC ?= gcc 2 | DEADBEEF_ROOT ?= /opt/deadbeef 3 | VGMSTREAM_ROOT ?= ./vgmstream 4 | VGMSTREAM_SOURCES := \ 5 | $(wildcard $(VGMSTREAM_ROOT)/src/*.c) \ 6 | $(wildcard $(VGMSTREAM_ROOT)/src/base/*.c) \ 7 | $(wildcard $(VGMSTREAM_ROOT)/src/coding/*.c) \ 8 | $(wildcard $(VGMSTREAM_ROOT)/src/coding/libs/*.c) \ 9 | $(wildcard $(VGMSTREAM_ROOT)/src/layout/*.c) \ 10 | $(wildcard $(VGMSTREAM_ROOT)/src/meta/*.c) \ 11 | $(wildcard $(VGMSTREAM_ROOT)/src/util/*.c) 12 | VGMSTREAM_OBJECTS := $(VGMSTREAM_SOURCES:.c=.o) 13 | PKGCONFIG_DEPS := libmpg123 vorbis vorbisfile libavcodec libavformat libavutil 14 | CFLAGS ?= \ 15 | -g -O2 \ 16 | -fvisibility=hidden \ 17 | $(shell pkg-config $(PKGCONFIG_DEPS) --cflags) 18 | INCLUDES ?= \ 19 | -I$(DEADBEEF_ROOT)/include \ 20 | -I$(VGMSTREAM_ROOT)/src \ 21 | -I$(VGMSTREAM_ROOT)/ext_includes 22 | DEFINES ?= \ 23 | -DVGM_USE_FFMPEG \ 24 | -DVGM_USE_VORBIS \ 25 | -DVGM_USE_MPEG 26 | LDFLAGS ?= \ 27 | $(shell pkg-config $(PKGCONFIG_DEPS) --libs) 28 | 29 | all: compile_flags.txt vgm.so 30 | 31 | clean: 32 | find . -name "*.o" -delete 33 | rm vgm.so 34 | 35 | compile_flags.txt: Makefile 36 | (echo $(CFLAGS) $(INCLUDES) $(DEFINES) | xargs -n1 echo) > $@ 37 | 38 | extensions.h: $(VGMSTREAM_ROOT)/src/formats.c 39 | awk '/ extension_list\[/,/}/{print}' $(VGMSTREAM_ROOT)/src/formats.c > $@ 40 | 41 | %.o: %.c 42 | $(CC) -c -o $@ $^ $(INCLUDES) $(DEFINES) $(CFLAGS) $(EXTRA_CFLAGS) 43 | 44 | vgm.o: vgm.c extensions.h 45 | $(CC) -c -o $@ vgm.c $(INCLUDES) $(DEFINES) $(CFLAGS) $(EXTRA_CFLAGS) 46 | 47 | vgm.so: $(VGMSTREAM_OBJECTS) vgm.o 48 | $(CC) -shared -o $@ $^ $(CFLAGS) $(LDFLAGS) $(EXTRA_LDFLAGS) 49 | 50 | install: vgm.so 51 | install -D vgm.so $(DEADBEEF_ROOT)/lib/deadbeef/vgm.so 52 | 53 | install-local: vgm.so 54 | install -D vgm.so $(HOME)/.local/lib/deadbeef/vgm.so 55 | 56 | .PHONY: all install install-local 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deadbeef-vgmstream 2 | deadbeef-vgmstream is a DeaDBeeF decoder plugin which connects vgmstream to DeaDBeeF, adding support for hundreds of streaming video game and middleware audio formats. 3 | 4 | ## Developing 5 | A Nix flake is included, as well as a corresponding `.envrc`. 6 | 7 | - Run a copy of Deadbeef with this plugin using `nix run`. 8 | - When using Nix to build, the NixOS version of VgmStream is used and the submodule is ignored. 9 | - When using `make` to build, the submodule version of VgmStream is used. 10 | 11 | To develop using Nix, ensure you have Nix installed and direnv configured, then type `direnv allow` to allow the environment to be applied. From there, you can run `make` like normal, and `nix run` to test inside of Deadbeef. 12 | 13 | Nix is not required to build. When not using Nix, you must ensure that mpg123, libvorbis, and FFmpeg are available via pkg-config, as these are requirements of VgmStream. 14 | 15 | When building using `make`, you can point to another VgmStream source path by setting the `VGMSTREAM_ROOT`. This will override the submodule. 16 | 17 | ## Installing 18 | By default, the Makefile is configured to install the plugin to a DeaDBeeF installation at the prefix, `/opt/deadbeef`. If you are using the official DeaDBeeF packages for Debian or Ubuntu, this should be fine. Otherwise, you'll need to adjust `DEADBEEF_ROOT` depending on where the program is installed to. Mind you that plugins are not installed directly to `DEADBEEF_ROOT` but under the `lib/deadbeef` subdirectory. 19 | -------------------------------------------------------------------------------- /extensions.h: -------------------------------------------------------------------------------- 1 | static const char* extension_list[] = { 2 | //"", /* vgmstream can play extensionless files too, but plugins must accept them manually */ 3 | 4 | "208", 5 | "2dx", 6 | "2dx9", 7 | "3do", 8 | "3ds", 9 | "4", //for Game.com audio 10 | "8", //txth/reserved [Gungage (PS1)] 11 | "800", 12 | "9tav", 13 | 14 | "a3c", //txth/reserved [Puyo Puyo 20th Anniversary (PSP)] 15 | //"aac", //common 16 | "aa3", //FFmpeg/not parsed (ATRAC3/ATRAC3PLUS/MP3/LPCM/WMA) 17 | "aax", 18 | "abc", //txth/reserved [Find My Own Way (PS2) tech demo] 19 | "abk", 20 | //"ac3", //common, FFmpeg/not parsed (AC3) 21 | "acb", 22 | "acm", 23 | "acx", 24 | "ad", //txth/reserved [Xenosaga Freaks (PS2)] 25 | "adc", //txth/reserved [Tomb Raider The Last Revelation (DC), Tomb Raider Chronicles (DC)] 26 | "adm", 27 | "adp", 28 | "adpcm", 29 | "adpcmx", 30 | "ads", 31 | "adw", 32 | "adx", 33 | "afc", 34 | "afs2", 35 | "agsc", 36 | "ahx", 37 | "ahv", 38 | "ai", 39 | //"aif", //common 40 | "aifc", //common? 41 | //"aiff", //common 42 | "aix", 43 | "akb", 44 | "al", //txth/raw [Dominions 3 - The Awakening (PC)] 45 | "al2", //txth/raw [Conquest of Elysium 3 (PC)] 46 | "amb", 47 | "ams", //txth/reserved [Super Dragon Ball Z (PS2) ELF names] 48 | "amx", 49 | "an2", 50 | "ao", 51 | "ap", 52 | "apc", 53 | "apm", 54 | "as4", 55 | "asbin", 56 | "asd", 57 | "asf", 58 | "asr", 59 | "ass", 60 | "ast", 61 | "at3", 62 | "at9", 63 | "atsl", 64 | "atsl3", 65 | "atsl4", 66 | "atslx", 67 | "atx", 68 | "aud", 69 | "audio", //txth/reserved [Grimm Echoes (Android)] 70 | "audio_data", 71 | "aus", 72 | "awa", //txth/reserved [Missing Parts Side A (PS2)] 73 | "awb", 74 | "awc", 75 | "awd", 76 | 77 | "b1s", //txth/reserved [7 Wonders of the Ancient World (PS2)] 78 | "baf", 79 | "baka", 80 | "bank", 81 | "bar", 82 | "bcstm", 83 | "bcwav", 84 | "bcv", //txth/reserved [The Bigs (PSP)] 85 | "bfstm", 86 | "bfwav", 87 | "bg00", 88 | "bgm", 89 | "bgw", 90 | "bigrp", 91 | "bik", 92 | "bika", //fake extension for .bik (to be removed) 93 | "bik2", 94 | "binka", //FFmpeg/not parsed (BINK AUDIO) 95 | //"bin", //common 96 | "bk2", 97 | "bkr", //txth/reserved [P.N.03 (GC), Viewtiful Joe (GC)] 98 | "blk", 99 | "bmdx", //fake extension (to be removed?) 100 | "bms", 101 | "bnk", 102 | "bnm", 103 | "bns", 104 | "bnsf", 105 | "bo2", 106 | "brstm", 107 | "brstmspm", 108 | "brwav", 109 | "brwsd", //fake extension for RWSD (non-format) 110 | "bsnd", 111 | "btsnd", 112 | "bvg", 113 | "bwav", 114 | 115 | "cads", 116 | "caf", 117 | "cat", 118 | "cbd2", 119 | "cbx", 120 | "cd", 121 | "cfn", //fake extension for CAF (renamed, to be removed?) 122 | "chd", //txth/reserved [Donkey Konga (GC), Star Fox Assault (GC)] 123 | "chk", 124 | "ckb", 125 | "ckd", 126 | "cks", 127 | "cnk", 128 | "cpk", 129 | "cps", 130 | "csa", //txth/reserved [LEGO Racers 2 (PS2)] 131 | "csb", 132 | "csmp", 133 | "cvs", //txth/reserved [Aladdin in Nasira's Revenge (PS1)] 134 | "cwav", 135 | "cxs", 136 | 137 | "d2", //txth/reserved [Dodonpachi Dai-Ou-Jou (PS2)] 138 | "da", 139 | //"dat", //common 140 | "data", 141 | "dax", 142 | "dbm", 143 | "dct", 144 | "dcs", 145 | "ddsp", 146 | "de2", 147 | "dec", 148 | "dic", 149 | "diva", 150 | "dmsg", //fake extension/header id for .sgt (to be removed) 151 | "ds2", //txth/reserved [Star Wars Bounty Hunter (GC)] 152 | "dsb", 153 | "dsf", 154 | "dsp", 155 | "dspw", 156 | "dtk", 157 | "dvi", 158 | "dyx", //txth/reserved [Shrek 4 (iOS)] 159 | 160 | "e4x", 161 | "eam", 162 | "eas", 163 | "eda", //txth/reserved [Project Eden (PS2)] 164 | "emff", //fake extension for .mul (to be removed) 165 | "enm", 166 | "eno", 167 | "ens", 168 | "esf", 169 | "exa", 170 | "ezw", 171 | 172 | "fag", 173 | "fcb", //FFmpeg/not parsed (BINK AUDIO) 174 | "fda", 175 | "filp", 176 | "fish", 177 | //"flac", //common 178 | "flx", 179 | "fsb", 180 | "fsv", 181 | "fwav", 182 | "fwse", 183 | 184 | "g1l", 185 | "gbts", 186 | "gca", 187 | "gcm", 188 | "gcub", 189 | "gcw", 190 | "ged", 191 | "genh", 192 | "gin", 193 | "gmd", //txth/semi [High Voltage games: Charlie and the Chocolate Factory (GC), Zathura (GC)] 194 | "gms", 195 | "grn", 196 | "gsf", 197 | "gsp", 198 | "gtd", 199 | "gwb", 200 | "gwm", 201 | 202 | "h4m", 203 | "hab", 204 | "hbd", 205 | "hca", 206 | "hd", 207 | "hd2", 208 | "hd3", 209 | "hdr", 210 | "hdt", 211 | "his", 212 | "hps", 213 | "hsf", 214 | "hvqm", 215 | "hwx", //txth/reserved [Star Wars Episode III (Xbox)] 216 | "hx2", 217 | "hx3", 218 | "hxc", 219 | "hxd", 220 | "hxg", 221 | "hxx", 222 | "hwas", 223 | "hwb", 224 | "hwd", 225 | 226 | "iab", 227 | "iadp", 228 | "idmsf", 229 | "idsp", 230 | "idvi", //fake extension/header id for .pcm (renamed, to be removed) 231 | "idwav", 232 | "idx", 233 | "idxma", 234 | "ifs", 235 | "ikm", 236 | "ild", 237 | "ilf", //txth/reserved [Madden NFL 98 (PS1)] 238 | "ilv", //txth/reserved [Star Wars Episode III (PS2)] 239 | "ima", 240 | "imc", 241 | "imf", 242 | "imx", 243 | "int", 244 | "is14", 245 | "isb", 246 | "isd", 247 | "isws", 248 | "itl", 249 | "ivaud", 250 | "ivag", 251 | "ivb", 252 | "ivs", //txth/reserved [Burnout 2 (PS2)] 253 | "ixa", 254 | 255 | "joe", 256 | "jstm", 257 | 258 | "k2sb", 259 | "ka1a", 260 | "kat", 261 | "kces", 262 | "kcey", //fake extension/header id for .pcm (renamed, to be removed) 263 | "km9", 264 | "kma", //txth/reserved [Dynasty Warriors 7: Empires (PS3)] 265 | "kmx", 266 | "kovs", //fake extension/header id for .kvs 267 | "kno", 268 | "kns", 269 | "koe", 270 | "kraw", 271 | "ktac", 272 | "ktsl2asbin", 273 | "ktss", //fake extension/header id for .kns 274 | "kvs", 275 | "kwa", 276 | 277 | "l", 278 | "l00", //txth/reserved [Disney's Dinosaur (PS2)] 279 | "laac", //fake extension for .aac (tri-Ace) 280 | "ladpcm", //not fake 281 | "laif", //fake extension for .aif (various) 282 | "laiff", //fake extension for .aiff 283 | "laifc", //fake extension for .aifc 284 | "lac3", //fake extension for .ac3, FFmpeg/not parsed 285 | "lasf", //fake extension for .asf (various) 286 | "lbin", //fake extension for .bin (various) 287 | "ldat", //fake extension for .dat 288 | "ldt", 289 | "lep", 290 | "lflac", //fake extension for .flac, FFmpeg/not parsed 291 | "lin", 292 | "lm0", 293 | "lm1", 294 | "lm2", 295 | "lm3", 296 | "lm4", 297 | "lm5", 298 | "lm6", 299 | "lm7", 300 | "lmp2", //fake extension for .mp2, FFmpeg/not parsed 301 | "lmp3", //fake extension for .mp3, FFmpeg/not parsed 302 | "lmp4", //fake extension for .mp4 303 | "lmpc", //fake extension for .mpc, FFmpeg/not parsed 304 | "logg", //fake extension for .ogg 305 | "lopus", //fake extension for .opus, used by LOPU too 306 | "lp", 307 | "lpcm", 308 | "lpk", 309 | "lps", 310 | "lrmh", 311 | "lse", 312 | "lsf", 313 | "lstm", //fake extension for .stm 314 | "lwav", //fake extension for .wav 315 | "lwd", 316 | "lwma", //fake extension for .wma, FFmpeg/not parsed 317 | 318 | "mab", 319 | "mad", 320 | "map", 321 | "mc3", 322 | "mca", 323 | "mcadpcm", 324 | "mcg", 325 | "mds", 326 | "mdsp", 327 | "med", 328 | "mjb", 329 | "mi4", //fake extension for .mib (renamed, to be removed) 330 | "mib", 331 | "mic", 332 | "mio", 333 | "mnstr", 334 | "mogg", 335 | //"m4a", //common 336 | //"m4v", //common 337 | //"mov", //common 338 | "move", 339 | //"mp+", //common [Moonshine Runners (PC)] 340 | //"mp2", //common 341 | //"mp3", //common 342 | //"mp4", //common 343 | //"mpc", //common 344 | "mpdsp", 345 | "mpds", 346 | "mpf", 347 | "mps", //txth/reserved [Scandal (PS2)] 348 | "ms", 349 | "msa", 350 | "msb", 351 | "msd", 352 | "mse", 353 | "msf", 354 | "mss", 355 | "msv", 356 | "msvp", //fake extension/header id for .msv 357 | "msx", 358 | "mta2", 359 | "mtaf", 360 | "mtt", //txth/reserved [Splinter Cell: Pandora Tomorrow (PS2)] 361 | "mul", 362 | "mups", 363 | "mus", 364 | "musc", 365 | "musx", 366 | "mvb", //txth/reserved [Porsche Challenge (PS1)] 367 | "mwa", //txth/reserved [Fatal Frame (Xbox)] 368 | "mwv", 369 | "mxst", 370 | "myspd", 371 | 372 | "n64", 373 | "naac", 374 | "nds", 375 | "ndp", //fake extension/header id for .nds 376 | "nlsd", 377 | "no", 378 | "nop", 379 | "nps", 380 | "npsf", //fake extension/header id for .nps (in bigfiles) 381 | "nsa", 382 | "nsopus", 383 | "nfx", 384 | "nub", 385 | "nub2", 386 | "nus3audio", 387 | "nus3bank", 388 | "nwa", 389 | "nwav", 390 | "nxa", 391 | "nxopus", 392 | 393 | "oga", 394 | //"ogg", //common 395 | "ogg_", 396 | "ogl", 397 | "ogs", 398 | "ogv", 399 | "oma", //FFmpeg/not parsed (ATRAC3/ATRAC3PLUS/MP3/LPCM/WMA) 400 | "omu", 401 | "oor", 402 | "opu", 403 | //"opus", //common 404 | "opusnx", 405 | "opusx", 406 | "oto", //txth/reserved [Vampire Savior (SAT)] 407 | "ovb", //txth/semi [namCollection: Tekken (PS2), Tekken 5: Tekken 1-3 (PS2)] 408 | 409 | "p04", //txth/reserved [Psychic Force 2012 (DC), Skies of Arcadia (DC)] 410 | "p08", //txth/reserved [SoulCalibur (DC)] 411 | "p16", //txth/reserved [Astal (SAT)] 412 | "p1d", //txth/reserved [Farming Simulator 18 (3DS)] 413 | "p2a", //txth/reserved [Thunderhawk Operation Phoenix (PS2)] 414 | "p2bt", 415 | "p3d", 416 | "paf", 417 | "past", 418 | "patch3audio", 419 | "pcm", 420 | "pdt", 421 | "phd", 422 | "pk", 423 | "pona", 424 | "pos", 425 | "ps3", 426 | "psb", 427 | "psf", 428 | "psh", //fake extension for .vsv (to be removed) 429 | "psn", 430 | "pwb", 431 | 432 | "qwv", //txth/reserved [Bishi Bashi Champ Online (AC)] 433 | 434 | "r", 435 | "rac", //txth/reserved [Manhunt (Xbox)] 436 | "rad", 437 | "rak", 438 | "ras", 439 | "raw", //txth/reserved [Madden NHL 97 (PC)-pcm8u] 440 | "rda", //FFmpeg/reserved [Rhythm Destruction (PC)] 441 | "res", //txth/reserved [Spider-Man: Web of Shadows (PSP)] 442 | "rkv", 443 | "rof", 444 | "rpgmvo", 445 | "rrds", 446 | "rsd", 447 | "rsf", 448 | "rsm", 449 | "rsnd", //txth/reserved [Birushana: Ichijuu no Kaze (Switch)] 450 | "rsp", 451 | "rstm", //fake extension/header id for .rstm (in bigfiles) 452 | "rvw", //txth/reserved [Half-Minute Hero (PC)] 453 | "rvws", 454 | "rwar", 455 | "rwav", 456 | "rws", 457 | "rwsd", 458 | "rwx", 459 | "rxx", //txth/reserved [Full Auto (X360)] 460 | 461 | "s14", 462 | "s3s", //txth/reserved [DT Racer (PS2)] 463 | "s3v", //Sound Voltex (AC) 464 | "sab", 465 | "sad", 466 | "saf", 467 | "sag", 468 | "sam", //txth/reserved [Lost Kingdoms 2 (GC)] 469 | "sap", 470 | "sb0", 471 | "sb1", 472 | "sb2", 473 | "sb3", 474 | "sb4", 475 | "sb5", 476 | "sb6", 477 | "sb7", 478 | "sbk", 479 | "sbin", 480 | "sbr", 481 | "sbv", 482 | "sig", 483 | "slb", //txth/reserved [Vingt-et-un Systems PS2 games (Last Escort, etc)] 484 | "sm0", 485 | "sm1", 486 | "sm2", 487 | "sm3", 488 | "sm4", 489 | "sm5", 490 | "sm6", 491 | "sm7", 492 | "sc", 493 | "scd", 494 | "sch", 495 | "sd9", 496 | "sdd", 497 | "sdl", 498 | "sdp", //txth/reserved [Metal Gear Arcade (AC)] 499 | "sdf", 500 | "sdt", 501 | "se", 502 | "seb", 503 | "sed", 504 | "seg", 505 | "sem", //txth/reserved [Oretachi Game Center Zoku: Sonic Wings (PS2)] 506 | "sf0", 507 | "sfl", 508 | "sfs", 509 | "sfx", 510 | "sgb", 511 | "sgd", 512 | "sgt", 513 | "shaa", 514 | "shsa", 515 | "skx", 516 | "slb", //txth/reserved [THE Nekomura no Hitobito (PS2)] 517 | "sli", 518 | "smc", 519 | "smk", 520 | "smp", 521 | "smv", 522 | "sn0", 523 | "snb", 524 | "snd", 525 | "snds", 526 | "sng", 527 | "sngw", 528 | "snr", 529 | "sns", 530 | "snu", 531 | "snz", //txth/reserved [Killzone HD (PS3)] 532 | "sod", 533 | "son", 534 | "spd", 535 | "spm", 536 | "sps", 537 | "spsd", 538 | "spw", 539 | "srsa", 540 | "ss2", 541 | "ssd", //txth/reserved [Zack & Wiki (Wii)] 542 | "ssf", 543 | "ssm", 544 | "sspr", 545 | "ssp", 546 | "sss", 547 | "ster", 548 | "sth", 549 | "stm", 550 | "str", 551 | "stream", 552 | "strm", 553 | "sts", 554 | "stv", //txth/reserved [Socio Art Logic PS2 games (Zero no Tsukaima games, Cambrian QTS, Shirogane no Soleil, etc)] 555 | "sts_cp3", 556 | "stx", 557 | "svag", 558 | "svs", 559 | "svg", 560 | "swag", 561 | "swav", 562 | "swd", 563 | "switch", //txth/reserved (.m4a-x.switch) [Ikinari Maou (Switch)] 564 | "switch_audio", 565 | "sx", 566 | "sxd", 567 | "sxd2", 568 | "sxd3", 569 | "szd", 570 | "szd1", 571 | "szd3", 572 | 573 | "tad", 574 | "tgq", 575 | "tgv", 576 | "thp", 577 | "tmx", 578 | "tra", 579 | "trk", 580 | "trs", //txth/semi [Kamiwaza (PS2), Shinobido (PS2)] 581 | "tun", 582 | "txth", 583 | "txtp", 584 | 585 | "u0", 586 | "ue4opus", 587 | "ulw", //txth/raw [Burnout (GC)] 588 | "um3", 589 | "utk", 590 | "uv", 591 | 592 | "v", 593 | "v0", 594 | //"v1", //dual channel with v0 595 | "va3", 596 | "vab", 597 | "vag", 598 | "vai", 599 | "vam", //txth/reserved [Rocket Power: Beach Bandits (PS2)] 600 | "vas", 601 | "vb", //txth/reserved [Tantei Jinguji Saburo: Mikan no Rupo (PS1)] 602 | "vbk", 603 | "vbx", //txth/reserved [THE Taxi 2 (PS2)] 604 | "vca", //txth/reserved [Pac-Man World (PS1)] 605 | "vcb", //txth/reserved [Pac-Man World (PS1)] 606 | "vds", 607 | "vdm", 608 | "vgi", //txth/reserved [Time Crisis II (PS2)] 609 | "vgm", //txth/reserved [Maximo (PS2)] 610 | "vgs", 611 | "vgv", 612 | "vh", 613 | "vid", 614 | "vig", 615 | "vis", 616 | "vm4", //txth/reserved [Elder Gate (PS1)] 617 | "vms", 618 | "vmu", //txth/reserved [Red Faction (PS2)] 619 | "voi", 620 | "vp6", 621 | "vpk", 622 | "vs", 623 | "vsf", 624 | "vsv", 625 | "vxn", 626 | 627 | "w", 628 | "waa", 629 | "wac", 630 | "wad", 631 | "waf", 632 | "wam", 633 | "was", 634 | //"wav", //common 635 | "wavc", 636 | "wave", 637 | "wavebatch", 638 | "wavm", 639 | "wavx", //txth/reserved [LEGO Star Wars (Xbox)] 640 | "wax", 641 | "way", 642 | "wb", 643 | "wb2", 644 | "wbd", 645 | "wbk", 646 | "wd", 647 | "wem", 648 | "wiive", //txth/semi [Rubik World (Wii)] 649 | "wic", //txth/reserved [Road Rash (SAT)-videos] 650 | "wip", //txth/reserved [Colin McRae DiRT (PC)] 651 | "wlv", //txth/reserved [ToeJam & Earl III: Mission to Earth (DC)] 652 | "wp2", 653 | "wpd", 654 | "wsd", 655 | "wsi", 656 | "wst", //txth/reserved [3jigen Shoujo o Hogo Shimashita (PC)] 657 | "wua", 658 | "wv2", 659 | "wv6", 660 | "wvd", //txth/reserved [Donkey Kong Barrel Blast (Wii)] 661 | "wve", 662 | "wvs", 663 | "wvx", 664 | "wxd", 665 | 666 | "x", 667 | "x360audio", //fake extension for Unreal Engine 3 XMA (real extension unknown) 668 | "xa", 669 | "xa2", 670 | "xa30", 671 | "xai", 672 | "xag", //txth/reserved [Tamsoft's PS2 games] 673 | "xau", 674 | "xav", 675 | "xb", //txth/reserved [Scooby-Doo! Unmasked (Xbox)] 676 | "xhd", 677 | "xen", 678 | "xma", 679 | "xma2", 680 | "xms", 681 | "xmu", 682 | "xmv", 683 | "xnb", 684 | "xsh", 685 | "xsf", 686 | "xst", 687 | "xse", 688 | "xsew", 689 | "xss", 690 | "xvag", 691 | "xwav", //fake extension for .wav (renamed, to be removed) 692 | "xwb", 693 | "xmd", 694 | "xopus", 695 | "xps", 696 | "xwc", 697 | "xwm", 698 | "xwma", 699 | "xws", 700 | "xwv", 701 | 702 | "ydsp", 703 | "ymf", 704 | 705 | "zic", 706 | "zsd", 707 | "zsm", 708 | "zss", 709 | "zwdsp", 710 | "zwv", 711 | 712 | "vgmstream" /* fake extension, catch-all for FFmpeg/txth/etc */ 713 | 714 | //, NULL //end mark 715 | }; 716 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1745930157, 24 | "narHash": "sha256-y3h3NLnzRSiUkYpnfvnS669zWZLoqqI6NprtLQ+5dck=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "46e634be05ce9dc6d4db8e664515ba10b78151ae", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Deadbeef plugin for playing streaming video game music"; 3 | inputs = { 4 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | }; 7 | outputs = 8 | { 9 | self, 10 | nixpkgs, 11 | flake-utils, 12 | ... 13 | }: 14 | flake-utils.lib.eachDefaultSystem ( 15 | system: 16 | let 17 | pkgs = import nixpkgs { inherit system; }; 18 | deadbeef-vgmstream = pkgs.callPackage ./package.nix { }; 19 | deadbeef-with-vgmstream = 20 | (pkgs.deadbeef-with-plugins.override { 21 | plugins = [ deadbeef-vgmstream ]; 22 | }).overrideAttrs 23 | { 24 | meta.mainProgram = "deadbeef"; 25 | }; 26 | in 27 | { 28 | packages = { 29 | inherit deadbeef-vgmstream deadbeef-with-vgmstream; 30 | default = deadbeef-with-vgmstream; 31 | }; 32 | checks = { 33 | inherit deadbeef-vgmstream; 34 | basic = pkgs.callPackage ./tests/basic.nix { inherit self; }; 35 | }; 36 | devShells.default = pkgs.mkShell { 37 | inputsFrom = [ deadbeef-vgmstream ]; 38 | }; 39 | } 40 | ) 41 | // { 42 | overlays.default = final: prev: { 43 | deadbeefPlugins = prev.deadbeefPlugins // { 44 | vgmstream = final.callPackage ./package.nix { }; 45 | }; 46 | }; 47 | nixosModules.deadbeef-vgmstream = { 48 | config = { 49 | nixpkgs.overlays = [ self.overlays.default ]; 50 | }; 51 | }; 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /package.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | stdenv, 4 | pkg-config, 5 | deadbeef, 6 | gtk3, 7 | vgmstream, 8 | mpg123, 9 | libvorbis, 10 | ffmpeg, 11 | }: 12 | 13 | stdenv.mkDerivation { 14 | pname = "deadbeef-vgmstream"; 15 | version = "unstable"; 16 | 17 | src = ./.; 18 | 19 | nativeBuildInputs = [ pkg-config ]; 20 | 21 | buildInputs = [ 22 | deadbeef 23 | gtk3 24 | mpg123 25 | libvorbis 26 | ffmpeg.dev 27 | ]; 28 | 29 | preBuild = '' 30 | cp --no-preserve=mode,ownership -LR ${vgmstream.src} ./vgmstream 31 | ''; 32 | 33 | enableParallelBuilding = true; 34 | 35 | installPhase = '' 36 | runHook preInstall 37 | 38 | mkdir -p $out/lib/deadbeef/ 39 | cp *.so $out/lib/deadbeef/ 40 | 41 | runHook postInstall 42 | ''; 43 | 44 | buildFlags = [ 45 | "DEADBEEF_ROOT=${deadbeef}" 46 | ]; 47 | 48 | meta = with lib; { 49 | description = "Streaming video game music plugin"; 50 | homepage = "https://github.com/jchv/deadbeef-vgmstream"; 51 | license = licenses.mit; 52 | maintainers = [ maintainers.jchw ]; 53 | platforms = platforms.linux; 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /tests/basic.nix: -------------------------------------------------------------------------------- 1 | { pkgs, self }: 2 | pkgs.nixosTest { 3 | nodes.machine = 4 | { pkgs, ... }: 5 | { 6 | imports = [ 7 | self.nixosModules.deadbeef-vgmstream 8 | (self.inputs.nixpkgs + "/nixos/tests/common/x11.nix") 9 | (self.inputs.nixpkgs + "/nixos/tests/common/user-account.nix") 10 | ]; 11 | test-support.displayManager.auto.user = "alice"; 12 | services.xserver.enable = true; 13 | environment.systemPackages = [ 14 | (pkgs.deadbeef-with-plugins.override { 15 | plugins = [ pkgs.deadbeefPlugins.vgmstream ]; 16 | }) 17 | ]; 18 | hardware.alsa = { 19 | enable = true; 20 | enableRecorder = true; 21 | defaultDevice.playback = "pcm.recorder"; 22 | }; 23 | systemd.services.audio-recorder = { 24 | script = "${pkgs.alsa-utils}/bin/arecord -Drecorder -fS16_LE -r48000 -c2 /tmp/record.wav"; 25 | }; 26 | system.stateVersion = "24.11"; 27 | }; 28 | name = "deadbeef-vgmstream-basic"; 29 | testScript = 30 | { nodes, ... }: 31 | let 32 | user = nodes.machine.users.users.alice; 33 | in 34 | '' 35 | from contextlib import contextmanager 36 | 37 | # This helper code is based on nixpkgs Firefox test. 38 | @contextmanager 39 | def record_audio(machine: Machine): 40 | machine.systemctl("start audio-recorder") 41 | yield 42 | machine.systemctl("stop audio-recorder") 43 | 44 | def wait_for_sound(machine: Machine): 45 | machine.wait_for_file("/tmp/record.wav") 46 | while True: 47 | machine.execute("tail -c 2M /tmp/record.wav > /tmp/last") 48 | size = int(machine.succeed("stat -c '%s' /tmp/last").strip()) 49 | status, output = machine.execute( 50 | f"cmp -i 50 -n {size - 50} /tmp/last /dev/zero 2>&1" 51 | ) 52 | if status == 1: 53 | break 54 | machine.sleep(2) 55 | 56 | machine.wait_for_x() 57 | machine.wait_for_file("${user.home}/.Xauthority") 58 | machine.succeed("xauth merge ${user.home}/.Xauthority") 59 | 60 | with subtest("Wait until DeaDBeeF starts up"): 61 | with record_audio(machine): 62 | machine.copy_from_host("${./test.adx}", "/tmp/test.adx") 63 | machine.execute("su - alice -c 'xterm -e deadbeef /tmp/test.adx' >&2 &") 64 | machine.wait_for_window("DeaDBeeF") 65 | machine.sleep(1) 66 | wait_for_sound(machine) 67 | ''; 68 | } 69 | -------------------------------------------------------------------------------- /tests/test.adx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchv/deadbeef-vgmstream/3e60464d10c954c39fd7d3cea28f20e247d55ba0/tests/test.adx -------------------------------------------------------------------------------- /vgm.c: -------------------------------------------------------------------------------- 1 | // vgmstream plugin for DeaDBeeF 2 | // Broken in two parts: need a SF implementation for VGMStream, and a decoder 3 | // implementation for Deadbeef. 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "extensions.h" 14 | 15 | #define trace(...) \ 16 | { \ 17 | fprintf(stderr, __VA_ARGS__); \ 18 | } 19 | 20 | static DB_decoder_t plugin; 21 | static DB_functions_t *deadbeef; 22 | 23 | #define DEFAULT_LOOP_COUNT 2.0 24 | #define DEFAULT_FADE_DURATION 10.0 25 | #define DEFAULT_FADE_DELAY 10.0 26 | 27 | #define DEFAULT_LOOP_COUNT_STR "2.0" 28 | #define DEFAULT_FADE_DURATION_STR "10.0" 29 | #define DEFAULT_FADE_DELAY_STR "10.0" 30 | 31 | static int conf_loop_single = 0; 32 | static double conf_loop_count = DEFAULT_LOOP_COUNT; 33 | static double conf_fade_duration = DEFAULT_FADE_DURATION; 34 | static double conf_fade_delay = DEFAULT_FADE_DELAY; 35 | static int conf_fade_single = 0; 36 | static int conf_fade_looping = 1; 37 | 38 | static const char settings_dlg[] = 39 | "property \"Loop count\" entry vgm.loopcount " DEFAULT_LOOP_COUNT_STR ";\n" 40 | "property \"Fade duration (seconds)\" entry " 41 | "vgm.fadeduration " DEFAULT_FADE_DURATION_STR ";\n" 42 | "property \"Fade delay (seconds)\" entry " 43 | "vgm.fadedelay " DEFAULT_FADE_DELAY_STR ";\n" 44 | "property \"Enable fade (single-shot)\" checkbox vgm.fadesingle 0;\n" 45 | "property \"Enable fade (looping)\" checkbox vgm.fadelooping 1;\n"; 46 | 47 | /* 48 | * VGMStream Streamfile implementation for Deadbeef 49 | */ 50 | 51 | typedef struct _DBSTREAMFILE { 52 | STREAMFILE sf; 53 | DB_FILE *file; 54 | off_t offset; 55 | char name[260]; 56 | } DBSTREAMFILE; 57 | 58 | static void dbsf_seek(DBSTREAMFILE *this, off_t offset) { 59 | if (!this->file) { 60 | return; 61 | } 62 | if (deadbeef->fseek(this->file, offset, SEEK_SET) == 0) { 63 | this->offset = offset; 64 | } else { 65 | this->offset = deadbeef->ftell(this->file); 66 | } 67 | } 68 | 69 | static off_t dbsf_get_size(DBSTREAMFILE *this) { 70 | if (!this->file) { 71 | return 0; 72 | } 73 | return deadbeef->fgetlength(this->file); 74 | } 75 | 76 | static off_t dbsf_get_offset(DBSTREAMFILE *this) { return this->offset; } 77 | 78 | static void dbsf_get_name(DBSTREAMFILE *this, char *buffer, size_t length) { 79 | strncpy(buffer, this->name, length); 80 | buffer[length - 1] = '\0'; 81 | } 82 | 83 | static size_t dbsf_read(DBSTREAMFILE *this, uint8_t *dest, off_t offset, 84 | size_t length) { 85 | size_t read; 86 | if (!this->file) { 87 | return 0; 88 | } 89 | if (this->offset != offset) { 90 | dbsf_seek(this, offset); 91 | } 92 | read = deadbeef->fread(dest, 1, length, this->file); 93 | if (read > 0) { 94 | this->offset += read; 95 | } 96 | return read; 97 | } 98 | 99 | static void dbsf_close(DBSTREAMFILE *this) { 100 | if (this->file) { 101 | deadbeef->fclose(this->file); 102 | this->file = NULL; 103 | } 104 | free(this); 105 | } 106 | 107 | static STREAMFILE *dbsf_create_from_path(const char *path); 108 | static STREAMFILE *dbsf_open(DBSTREAMFILE *this, const char *const filename, 109 | size_t buffersize) { 110 | if (!filename) 111 | return NULL; 112 | return dbsf_create_from_path(filename); 113 | } 114 | 115 | static STREAMFILE *dbsf_create(DB_FILE *file, const char *path) { 116 | DBSTREAMFILE *streamfile = malloc(sizeof(DBSTREAMFILE)); 117 | 118 | if (!streamfile) 119 | return NULL; 120 | 121 | memset(streamfile, 0, sizeof(DBSTREAMFILE)); 122 | streamfile->sf.read = (void *)dbsf_read; 123 | streamfile->sf.get_size = (void *)dbsf_get_size; 124 | streamfile->sf.get_offset = (void *)dbsf_get_offset; 125 | streamfile->sf.get_name = (void *)dbsf_get_name; 126 | streamfile->sf.open = (void *)dbsf_open; 127 | streamfile->sf.close = (void *)dbsf_close; 128 | streamfile->file = file; 129 | streamfile->offset = 0; 130 | strncpy(streamfile->name, path, sizeof(streamfile->name)); 131 | 132 | return &streamfile->sf; 133 | } 134 | 135 | STREAMFILE *dbsf_create_from_path(const char *path) { 136 | DB_FILE *file = deadbeef->fopen(path); 137 | 138 | if (!file) { 139 | /* Allow vgmstream's virtual files. */ 140 | if (!vgmstream_is_virtual_filename(path)) { 141 | return NULL; 142 | } 143 | } 144 | 145 | return dbsf_create(file, path); 146 | } 147 | 148 | VGMSTREAM *init_vgmstream_from_dbfile(const char *path, int subsong) { 149 | STREAMFILE *sf; 150 | VGMSTREAM *vgm; 151 | 152 | sf = dbsf_create_from_path(path); 153 | if (!sf) 154 | return NULL; 155 | 156 | sf->stream_index = subsong; 157 | 158 | vgm = init_vgmstream_from_STREAMFILE(sf); 159 | if (!vgm) 160 | goto err1; 161 | 162 | return vgm; 163 | err1: 164 | dbsf_close((DBSTREAMFILE *)sf); 165 | return NULL; 166 | } 167 | 168 | /* 169 | * VGMStream decoder plugin for Deadbeef 170 | */ 171 | 172 | typedef struct { 173 | DB_fileinfo_t info; 174 | VGMSTREAM *s; 175 | int can_loop; 176 | int position; 177 | int fadesamples; 178 | int totalsamples; 179 | } vgm_info_t; 180 | 181 | #define COPYRIGHT_STR \ 182 | "deadbeef-vgmstream\n" \ 183 | "Copyright (c) 2014-2025 John Chadwick \n" \ 184 | "\n" \ 185 | "Licensed under the same terms as vgmstream (see below.)\n" \ 186 | "\n" \ 187 | "vgmstream\n" \ 188 | "Copyright (c) 2008-2010 Adam Gashlin, Fastelbja, Ronny Elfert\n" \ 189 | "Portions Copyright (c) 2004-2008, Marko Kreen\n" \ 190 | "Portions Copyright 2001-2007 jagarl / Kazunori Ueno " \ 191 | "\n" \ 192 | "Portions Copyright (c) 1998, Justin Frankel/Nullsoft Inc.\n" \ 193 | "Portions Copyright (C) 2006 Nullsoft, Inc.\n" \ 194 | "Portions Copyright (c) 2005-2007 Paul Hsieh\n" \ 195 | "Portions Public Domain originating with Sun Microsystems\n" \ 196 | "\n" \ 197 | "Permission to use, copy, modify, and distribute this software for any\n" \ 198 | "purpose with or without fee is hereby granted, provided that the above\n" \ 199 | "copyright notice and this permission notice appear in all copies.\n" \ 200 | "\n" \ 201 | "THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL " \ 202 | "WARRANTIES\n" \ 203 | "WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n" \ 204 | "MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n" \ 205 | "ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n" \ 206 | "WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n" \ 207 | "ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\n" \ 208 | "OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n" 209 | 210 | static DB_fileinfo_t *vgm_open(uint32_t hints) { 211 | vgm_info_t *info = malloc(sizeof(vgm_info_t)); 212 | memset(info, 0, sizeof(vgm_info_t)); 213 | info->can_loop = (hints & DDB_DECODER_HINT_CAN_LOOP) != 0; 214 | return &info->info; 215 | } 216 | 217 | static int vgm_init(DB_fileinfo_t *_info, DB_playItem_t *it) { 218 | vgm_info_t *info = (vgm_info_t *)_info; 219 | deadbeef->pl_lock(); 220 | char *fname = strdup(deadbeef->pl_find_meta(it, ":URI")); 221 | int subsong = deadbeef->pl_find_meta_int(it, ":TRACKNUM", 0); 222 | deadbeef->pl_unlock(); 223 | 224 | info->s = init_vgmstream_from_dbfile(fname, subsong != 0 ? subsong : 1); 225 | free(fname); 226 | 227 | if (!info->s) { 228 | return -1; 229 | } 230 | 231 | info->fadesamples = conf_fade_duration * info->s->sample_rate; 232 | info->totalsamples = get_vgmstream_play_samples( 233 | conf_loop_count, conf_fade_duration, conf_fade_delay, info->s); 234 | 235 | _info->readpos = 0; 236 | _info->plugin = &plugin; 237 | _info->fmt.bps = 16; 238 | _info->fmt.channels = info->s->channels; 239 | _info->fmt.samplerate = info->s->sample_rate; 240 | 241 | int i; 242 | for (i = 0; i < _info->fmt.channels; i++) { 243 | _info->fmt.channelmask |= 1 << i; 244 | } 245 | 246 | return 0; 247 | } 248 | 249 | static void vgm_free(DB_fileinfo_t *_info) { 250 | vgm_info_t *info = (vgm_info_t *)_info; 251 | close_vgmstream(info->s); 252 | if (info) { 253 | free(info); 254 | } 255 | } 256 | 257 | static int vgm_read(DB_fileinfo_t *_info, char *bytes, int size) { 258 | int i, j; 259 | vgm_info_t *info = (vgm_info_t *)_info; 260 | int sample_size = _info->fmt.channels * sizeof(int16_t); 261 | int32_t sample_count = size / sample_size, 262 | fade_start = info->totalsamples - info->fadesamples, 263 | fade_end = info->totalsamples, fade_samples = info->fadesamples; 264 | 265 | int terminate = !conf_loop_single || !info->can_loop || !info->s->loop_flag; 266 | int is_looping = info->s->loop_flag; 267 | int do_fade = ((conf_fade_single && !is_looping) || 268 | (conf_fade_looping && is_looping)) && terminate; 269 | 270 | if (terminate) { 271 | /* Past the end? Let deadbeef know. */ 272 | if (info->position >= info->totalsamples) { 273 | return 0; 274 | } 275 | } 276 | 277 | /* Do the actual rendering. */ 278 | sample_count = render_vgmstream((int16_t *)bytes, sample_count, info->s); 279 | 280 | /* Update position. */ 281 | info->position += sample_count; 282 | _info->readpos = (float)info->position / (float)_info->fmt.samplerate; 283 | 284 | /* If we are overlapping any piece after the fade starts, we must fade out 285 | * here. 286 | * TODO: Code should be refactored to be more clear. 287 | */ 288 | if (do_fade && (info->position > fade_start || 289 | info->position + sample_count > fade_start)) { 290 | int16_t *buf = (int16_t *)bytes; 291 | for (i = 0; i < sample_count; ++i) { 292 | int pos = i + info->position; 293 | if (pos > fade_start) { 294 | int samples_into_fade = pos - fade_start; 295 | double fadedness = 296 | (double)(fade_samples - samples_into_fade) / fade_samples; 297 | for (j = 0; j < info->s->channels; j++) { 298 | buf[i * info->s->channels + j] = 299 | buf[i * info->s->channels + j] * fadedness; 300 | } 301 | } else if (pos > fade_end) { 302 | break; 303 | } 304 | } 305 | } 306 | 307 | return sample_count * sample_size; 308 | } 309 | 310 | static int vgm_seek_sample(DB_fileinfo_t *_info, int sample) { 311 | int i; 312 | vgm_info_t *info = (vgm_info_t *)_info; 313 | int16_t samples[0x1000 * _info->fmt.channels]; 314 | 315 | /* Reset stream to go backwards */ 316 | if (sample < info->position) { 317 | reset_vgmstream(info->s); 318 | info->position = 0; 319 | _info->readpos = 0; 320 | } 321 | 322 | int seek = sample - info->position; 323 | 324 | /* Render out 0x1000 sample chunks and discard them */ 325 | while (seek > 0x1000) { 326 | vgm_read(_info, (char *)samples, 327 | 0x1000 * _info->fmt.channels * sizeof(int16_t)); 328 | seek -= 0x1000; 329 | } 330 | 331 | /* Read the final chunk of < 0x1000 samples (or not) */ 332 | if (seek > 0) { 333 | vgm_read(_info, (char *)samples, 334 | seek * _info->fmt.channels * sizeof(int16_t)); 335 | } 336 | 337 | return 0; 338 | } 339 | 340 | static int vgm_seek(DB_fileinfo_t *_info, float time) { 341 | return vgm_seek_sample(_info, time * _info->fmt.samplerate); 342 | } 343 | 344 | static DB_playItem_t *vgm_insert_subsong(ddb_playlist_t *plt, 345 | DB_playItem_t *after, 346 | const char *fname, int subsong) { 347 | VGMSTREAM *vgm = init_vgmstream_from_dbfile(fname, subsong); 348 | if (!vgm) { 349 | return after; 350 | } 351 | 352 | DB_playItem_t *it = deadbeef->pl_item_alloc_init(fname, plugin.plugin.id); 353 | 354 | vgmstream_title_t tcfg; 355 | memset(&tcfg, 0, sizeof(vgmstream_title_t)); 356 | tcfg.remove_extension = 1; 357 | 358 | char title[1024]; 359 | 360 | vgmstream_get_title(title, sizeof(title), fname, vgm, &tcfg); 361 | deadbeef->pl_add_meta(it, "title", title); 362 | deadbeef->pl_replace_meta(it, ":FILETYPE", "vgm"); 363 | deadbeef->pl_set_meta_int(it, ":TRACKNUM", subsong); 364 | if (vgm->loop_flag) { 365 | deadbeef->pl_set_meta_int(it, ":loop_start", vgm->loop_start_sample); 366 | deadbeef->pl_set_meta_int(it, ":loop_end", vgm->loop_end_sample); 367 | } 368 | if (vgm->num_streams > 1) { 369 | deadbeef->pl_set_meta_int(it, ":stream_count", vgm->num_streams); 370 | } 371 | if (vgm->stream_index > 1) { 372 | deadbeef->pl_set_meta_int(it, ":stream_index", vgm->stream_index); 373 | } 374 | if (vgm->stream_name != NULL && *vgm->stream_name != '\0') { 375 | deadbeef->pl_add_meta(it, ":stream_name", vgm->stream_name); 376 | } 377 | deadbeef->pl_set_meta_int(it, ":SAMPLERATE", vgm->sample_rate); 378 | deadbeef->pl_set_meta_int(it, ":CHANNELS", vgm->channels); 379 | deadbeef->pl_set_meta_int(it, ":BPS", 16); 380 | deadbeef->pl_set_meta_int(it, ":BITRATE", 381 | get_vgmstream_average_bitrate(vgm) / 1000); 382 | 383 | size_t num_samples = get_vgmstream_play_samples( 384 | conf_loop_count, conf_fade_duration, conf_fade_delay, vgm); 385 | deadbeef->plt_set_item_duration(plt, it, 386 | (float)num_samples / vgm->sample_rate); 387 | 388 | after = deadbeef->plt_insert_item(plt, after, it); 389 | deadbeef->pl_item_unref(it); 390 | 391 | close_vgmstream(vgm); 392 | 393 | return after; 394 | } 395 | 396 | static DB_playItem_t *vgm_insert(ddb_playlist_t *plt, DB_playItem_t *after, 397 | const char *fname) { 398 | VGMSTREAM *vgm = init_vgmstream_from_dbfile(fname, 0); 399 | if (vgm == NULL) { 400 | return after; 401 | } 402 | 403 | int i, num_subsongs = vgm->num_streams > 0 ? vgm->num_streams : 1; 404 | int add_subsong = (num_subsongs > 1 && vgm->stream_index == 0) 405 | ? -1 406 | : (vgm->stream_index || 1); 407 | 408 | close_vgmstream(vgm); 409 | 410 | if (add_subsong == -1) { 411 | for (i = 1; i <= num_subsongs; ++i) { 412 | after = vgm_insert_subsong(plt, after, fname, i); 413 | } 414 | } else { 415 | after = vgm_insert_subsong(plt, after, fname, add_subsong); 416 | } 417 | 418 | return after; 419 | } 420 | 421 | static void vgm_reload_config(void) { 422 | conf_loop_single = 423 | deadbeef->conf_get_int("playback.loop", PLAYBACK_MODE_LOOP_ALL) == 424 | PLAYBACK_MODE_LOOP_SINGLE; 425 | conf_loop_count = (double)deadbeef->conf_get_float("vgm.loopcount", 426 | (float)DEFAULT_LOOP_COUNT); 427 | conf_fade_duration = (double)deadbeef->conf_get_float( 428 | "vgm.fadeduration", (float)DEFAULT_FADE_DURATION); 429 | conf_fade_delay = (double)deadbeef->conf_get_float("vgm.fadedelay", 430 | (float)DEFAULT_FADE_DELAY); 431 | conf_fade_single = deadbeef->conf_get_int("vgm.fadesingle", 0); 432 | conf_fade_looping = deadbeef->conf_get_int("vgm.fadelooping", 1); 433 | } 434 | 435 | static int vgm_start(void) { 436 | vgm_reload_config(); 437 | return 0; 438 | } 439 | 440 | static int vgm_stop(void) { return 0; } 441 | 442 | static int vgm_message(uint32_t id, uintptr_t ctx, uint32_t p1, uint32_t p2) { 443 | switch (id) { 444 | case DB_EV_CONFIGCHANGED: 445 | vgm_reload_config(); 446 | break; 447 | } 448 | return 0; 449 | } 450 | 451 | // define plugin interface 452 | static DB_decoder_t plugin = { 453 | DB_PLUGIN_SET_API_VERSION.plugin.version_major = 0, 454 | .plugin.version_minor = 1, 455 | .plugin.type = DB_PLUGIN_DECODER, 456 | .plugin.name = "vgmstream", 457 | .plugin.id = "vgm", 458 | .plugin.descr = "Decodes a variety of streaming video game music formats.", 459 | .plugin.copyright = COPYRIGHT_STR, 460 | .plugin.start = vgm_start, 461 | .plugin.stop = vgm_stop, 462 | .plugin.configdialog = settings_dlg, 463 | .plugin.message = vgm_message, 464 | .open = vgm_open, 465 | .init = vgm_init, 466 | .free = vgm_free, 467 | .read = vgm_read, 468 | .seek = vgm_seek, 469 | .seek_sample = vgm_seek_sample, 470 | .insert = vgm_insert, 471 | .exts = extension_list, 472 | }; 473 | 474 | __attribute__((visibility("default"))) DB_plugin_t * 475 | vgm_load(DB_functions_t *api) { 476 | deadbeef = api; 477 | return DB_PLUGIN(&plugin); 478 | } 479 | --------------------------------------------------------------------------------