├── LICENSE ├── README.txt ├── index.html ├── mods ├── RandomVoice-Monday.mod ├── ambpower.mod ├── dope.mod ├── frust.mod ├── mindkick.mod ├── ode_to_protracker.mod └── sundance.mod ├── src ├── modfile.js ├── modplayer.js └── xmfile.js ├── testfiles.txt └── tests ├── basic.mod ├── break.mod ├── break_far.mod ├── cut.mod ├── ex_loop.mod ├── fineporta.mod └── jump.mod /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2010 by Matt Westcott 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | 2 | Effects implemented: 3 | 4 | 0xy: Arpeggio 5 | 1xx: Pitch slide up (portamento) 6 | 2xx: Pitch slide down 7 | 3xx: Portamento to note [slightly buggy] 8 | 5xy: Portamento to note with volume slide [untested] 9 | 9xx: Sample offset 10 | Axy: Volume slide 11 | Bxx: Jump to order 12 | Cxx: Set note volume 13 | Dxx: Pattern break 14 | Fxx: Set BPM 15 | Exy Subcommands: 16 | E1x Fine portamento up 17 | E2x Fine portamento down 18 | E5x Set note fine-tune [untested] 19 | E6x Pattern loop 20 | E9x Re-trigger note 21 | EAx Fine volume slide up 22 | EBx Fine volume slide down 23 | ECx Note cut 24 | EDx Note delay 25 | EEx Pattern delay 26 | 27 | 28 | In progress: 29 | 30 | 4xy Vibrato 31 | 6xy Vibrato with volume slide 32 | 7xy Tremolo 33 | 8xx Set note panning position 34 | Exy Subcommands: 35 | E8x Set note panning position 36 | 37 | 38 | 39 | Not implemented: 40 | 41 | Exy Subcommands: 42 | E0x Amiga LED Filter toggle 43 | E3x Glissando control 44 | E4x Vibrato control 45 | E7x Tremolo control 46 | EFx Funk it! 47 | 48 | http://www.milkytracker.org/docs/MilkyTracker.html#effects 49 | 50 | ========================================================== 51 | 52 | TODO: 53 | 54 | - Implement additional effects. Try to pass the Ode to Protracker test :) 55 | - Wean off dynamicaudio.js (eventually) 56 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 10 | 11 | 94 | 95 | 96 |This is a work in progress - not all effects are implemented yet.
99 |Original by Gasman. 113 | @westdotcodottt - 114 | matt.west.co.tt - 115 | gasman@raww.org - 22 May 2010 116 |
117 |Forked and improved by
118 | Billy Wenge-Murphy -
119 | @BillyWM -
120 | iamthebilly@gmail.com - April 2011.
121 | Added local file loading (HTML5 File API), implemented more effects, fixes to playback
122 |
Source code on Github: 124 | Gasman (original) 125 | BillyWM (fork) 126 |
127 | 128 | 129 | -------------------------------------------------------------------------------- /mods/RandomVoice-Monday.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BillyWM/jsmodplayer/b33600b0e55a7ba33d2bef9ab6f0767097c2a081/mods/RandomVoice-Monday.mod -------------------------------------------------------------------------------- /mods/ambpower.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BillyWM/jsmodplayer/b33600b0e55a7ba33d2bef9ab6f0767097c2a081/mods/ambpower.mod -------------------------------------------------------------------------------- /mods/dope.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BillyWM/jsmodplayer/b33600b0e55a7ba33d2bef9ab6f0767097c2a081/mods/dope.mod -------------------------------------------------------------------------------- /mods/frust.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BillyWM/jsmodplayer/b33600b0e55a7ba33d2bef9ab6f0767097c2a081/mods/frust.mod -------------------------------------------------------------------------------- /mods/mindkick.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BillyWM/jsmodplayer/b33600b0e55a7ba33d2bef9ab6f0767097c2a081/mods/mindkick.mod -------------------------------------------------------------------------------- /mods/ode_to_protracker.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BillyWM/jsmodplayer/b33600b0e55a7ba33d2bef9ab6f0767097c2a081/mods/ode_to_protracker.mod -------------------------------------------------------------------------------- /mods/sundance.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BillyWM/jsmodplayer/b33600b0e55a7ba33d2bef9ab6f0767097c2a081/mods/sundance.mod -------------------------------------------------------------------------------- /src/modfile.js: -------------------------------------------------------------------------------- 1 | var channelCountByIdentifier = { 2 | 'TDZ1': 1, '1CHN': 1, 'TDZ2': 2, '2CHN': 2, 'TDZ3': 3, '3CHN': 3, 3 | 'M.K.': 4, 'FLT4': 4, 'M!K!': 4, '4CHN': 4, 'TDZ4': 4, '5CHN': 5, 'TDZ5': 5, 4 | '6CHN': 6, 'TDZ6': 6, '7CHN': 7, 'TDZ7': 7, '8CHN': 8, 'TDZ8': 8, 'OCTA': 8, 'CD81': 8, 5 | '9CHN': 9, 'TDZ9': 9, 6 | '10CH': 10, '11CH': 11, '12CH': 12, '13CH': 13, '14CH': 14, '15CH': 15, '16CH': 16, '17CH': 17, 7 | '18CH': 18, '19CH': 19, '20CH': 20, '21CH': 21, '22CH': 22, '23CH': 23, '24CH': 24, '25CH': 25, 8 | '26CH': 26, '27CH': 27, '28CH': 28, '29CH': 29, '30CH': 30, '31CH': 31, '32CH': 32 9 | } 10 | 11 | function ModFile(mod) { 12 | function trimNulls(str) { 13 | return str.replace(/\x00+$/, ''); 14 | } 15 | function getWord(str, pos) { 16 | return (str.charCodeAt(pos) << 8) + str.charCodeAt(pos+1) 17 | } 18 | 19 | this.data = mod; 20 | this.samples = []; 21 | this.sampleData = []; 22 | this.positions = []; 23 | this.patternCount = 0; 24 | this.patterns = []; 25 | 26 | this.title = trimNulls(mod.substr(0, 20)) 27 | 28 | this.sampleCount = 31; 29 | 30 | for (var i = 0; i < this.sampleCount; i++) { 31 | var sampleInfo = mod.substr(20 + i*30, 30); 32 | var sampleName = trimNulls(sampleInfo.substr(0, 22)); 33 | this.samples[i] = { 34 | length: getWord(sampleInfo, 22) * 2, 35 | finetune: sampleInfo.charCodeAt(24), 36 | volume: sampleInfo.charCodeAt(25), 37 | repeatOffset: getWord(sampleInfo, 26) * 2, 38 | repeatLength: getWord(sampleInfo, 28) * 2, 39 | } 40 | } 41 | 42 | this.positionCount = mod.charCodeAt(950); 43 | this.positionLoopPoint = mod.charCodeAt(951); 44 | for (var i = 0; i < 128; i++) { 45 | this.positions[i] = mod.charCodeAt(952+i); 46 | if (this.positions[i] >= this.patternCount) { 47 | this.patternCount = this.positions[i]+1; 48 | } 49 | } 50 | 51 | var identifier = mod.substr(1080, 4); 52 | 53 | this.channelCount = channelCountByIdentifier[identifier]; 54 | if (!this.channelCount) { 55 | this.channelCount = 4; 56 | } 57 | 58 | var patternOffset = 1084; 59 | for (var pat = 0; pat < this.patternCount; pat++) { 60 | this.patterns[pat] = []; 61 | for (var row = 0; row < 64; row++) { 62 | this.patterns[pat][row] = []; 63 | for (var chan = 0; chan < this.channelCount; chan++) { 64 | b0 = mod.charCodeAt(patternOffset); 65 | b1 = mod.charCodeAt(patternOffset + 1); 66 | b2 = mod.charCodeAt(patternOffset + 2); 67 | b3 = mod.charCodeAt(patternOffset + 3); 68 | var eff = b2 & 0x0f; 69 | this.patterns[pat][row][chan] = { 70 | sample: (b0 & 0xf0) | (b2 >> 4), 71 | period: ((b0 & 0x0f) << 8) | b1, 72 | effect: eff, 73 | effectParameter: b3 74 | }; 75 | if (eff == 0x0E) { 76 | this.patterns[pat][row][chan].extEffect = (b3 & 0xF0) >> 4; 77 | this.patterns[pat][row][chan].extEffectParameter = (b3 & 0x0F); 78 | } 79 | patternOffset += 4; 80 | } 81 | } 82 | } 83 | 84 | var sampleOffset = patternOffset; 85 | for (var s = 0; s < this.sampleCount; s++) { 86 | this.samples[s].startOffset = sampleOffset; 87 | this.sampleData[s] = new Uint8Array(this.samples[s].length); 88 | var i = 0; 89 | for (var o = sampleOffset, e = sampleOffset + this.samples[s].length; o < e; o++) { 90 | this.sampleData[s][i] = mod.charCodeAt(o); 91 | i++; 92 | } 93 | sampleOffset += this.samples[s].length; 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /src/modplayer.js: -------------------------------------------------------------------------------- 1 | /* 2 | Useful docs 3 | Explains effect calculations: http://www.mediatel.lu/workshop/audio/fileformat/h_mod.html 4 | 5 | */ 6 | 7 | /* 8 | ModPeriodTable[ft][n] = the period to use for note number n at finetune value ft. 9 | Finetune values are in twos-complement, i.e. [0,1,2,3,4,5,6,7,-8,-7,-6,-5,-4,-3,-2,-1] 10 | The first table is used to generate a reverse lookup table, to find out the note number 11 | for a period given in the MOD file. 12 | */ 13 | var ModPeriodTable = [ 14 | [1712, 1616, 1524, 1440, 1356, 1280, 1208, 1140, 1076, 1016, 960 , 906, 15 | 856 , 808 , 762 , 720 , 678 , 640 , 604 , 570 , 538 , 508 , 480 , 453, 16 | 428 , 404 , 381 , 360 , 339 , 320 , 302 , 285 , 269 , 254 , 240 , 226, 17 | 214 , 202 , 190 , 180 , 170 , 160 , 151 , 143 , 135 , 127 , 120 , 113, 18 | 107 , 101 , 95 , 90 , 85 , 80 , 75 , 71 , 67 , 63 , 60 , 56 ], 19 | [1700, 1604, 1514, 1430, 1348, 1274, 1202, 1134, 1070, 1010, 954 , 900, 20 | 850 , 802 , 757 , 715 , 674 , 637 , 601 , 567 , 535 , 505 , 477 , 450, 21 | 425 , 401 , 379 , 357 , 337 , 318 , 300 , 284 , 268 , 253 , 239 , 225, 22 | 213 , 201 , 189 , 179 , 169 , 159 , 150 , 142 , 134 , 126 , 119 , 113, 23 | 106 , 100 , 94 , 89 , 84 , 79 , 75 , 71 , 67 , 63 , 59 , 56 ], 24 | [1688, 1592, 1504, 1418, 1340, 1264, 1194, 1126, 1064, 1004, 948 , 894, 25 | 844 , 796 , 752 , 709 , 670 , 632 , 597 , 563 , 532 , 502 , 474 , 447, 26 | 422 , 398 , 376 , 355 , 335 , 316 , 298 , 282 , 266 , 251 , 237 , 224, 27 | 211 , 199 , 188 , 177 , 167 , 158 , 149 , 141 , 133 , 125 , 118 , 112, 28 | 105 , 99 , 94 , 88 , 83 , 79 , 74 , 70 , 66 , 62 , 59 , 56 ], 29 | [1676, 1582, 1492, 1408, 1330, 1256, 1184, 1118, 1056, 996 , 940 , 888, 30 | 838 , 791 , 746 , 704 , 665 , 628 , 592 , 559 , 528 , 498 , 470 , 444, 31 | 419 , 395 , 373 , 352 , 332 , 314 , 296 , 280 , 264 , 249 , 235 , 222, 32 | 209 , 198 , 187 , 176 , 166 , 157 , 148 , 140 , 132 , 125 , 118 , 111, 33 | 104 , 99 , 93 , 88 , 83 , 78 , 74 , 70 , 66 , 62 , 59 , 55 ], 34 | [1664, 1570, 1482, 1398, 1320, 1246, 1176, 1110, 1048, 990 , 934 , 882, 35 | 832 , 785 , 741 , 699 , 660 , 623 , 588 , 555 , 524 , 495 , 467 , 441, 36 | 416 , 392 , 370 , 350 , 330 , 312 , 294 , 278 , 262 , 247 , 233 , 220, 37 | 208 , 196 , 185 , 175 , 165 , 156 , 147 , 139 , 131 , 124 , 117 , 110, 38 | 104 , 98 , 92 , 87 , 82 , 78 , 73 , 69 , 65 , 62 , 58 , 55 ], 39 | [1652, 1558, 1472, 1388, 1310, 1238, 1168, 1102, 1040, 982 , 926 , 874, 40 | 826 , 779 , 736 , 694 , 655 , 619 , 584 , 551 , 520 , 491 , 463 , 437, 41 | 413 , 390 , 368 , 347 , 328 , 309 , 292 , 276 , 260 , 245 , 232 , 219, 42 | 206 , 195 , 184 , 174 , 164 , 155 , 146 , 138 , 130 , 123 , 116 , 109, 43 | 103 , 97 , 92 , 87 , 82 , 77 , 73 , 69 , 65 , 61 , 58 , 54 ], 44 | [1640, 1548, 1460, 1378, 1302, 1228, 1160, 1094, 1032, 974 , 920 , 868, 45 | 820 , 774 , 730 , 689 , 651 , 614 , 580 , 547 , 516 , 487 , 460 , 434, 46 | 410 , 387 , 365 , 345 , 325 , 307 , 290 , 274 , 258 , 244 , 230 , 217, 47 | 205 , 193 , 183 , 172 , 163 , 154 , 145 , 137 , 129 , 122 , 115 , 109, 48 | 102 , 96 , 91 , 86 , 81 , 77 , 72 , 68 , 64 , 61 , 57 , 54 ], 49 | [1628, 1536, 1450, 1368, 1292, 1220, 1150, 1086, 1026, 968 , 914 , 862, 50 | 814 , 768 , 725 , 684 , 646 , 610 , 575 , 543 , 513 , 484 , 457 , 431, 51 | 407 , 384 , 363 , 342 , 323 , 305 , 288 , 272 , 256 , 242 , 228 , 216, 52 | 204 , 192 , 181 , 171 , 161 , 152 , 144 , 136 , 128 , 121 , 114 , 108, 53 | 102 , 96 , 90 , 85 , 80 , 76 , 72 , 68 , 64 , 60 , 57 , 54 ], 54 | [1814, 1712, 1616, 1524, 1440, 1356, 1280, 1208, 1140, 1076, 1016, 960, 55 | 907 , 856 , 808 , 762 , 720 , 678 , 640 , 604 , 570 , 538 , 508 , 480, 56 | 453 , 428 , 404 , 381 , 360 , 339 , 320 , 302 , 285 , 269 , 254 , 240, 57 | 226 , 214 , 202 , 190 , 180 , 170 , 160 , 151 , 143 , 135 , 127 , 120, 58 | 113 , 107 , 101 , 95 , 90 , 85 , 80 , 75 , 71 , 67 , 63 , 60 ], 59 | [1800, 1700, 1604, 1514, 1430, 1350, 1272, 1202, 1134, 1070, 1010, 954, 60 | 900 , 850 , 802 , 757 , 715 , 675 , 636 , 601 , 567 , 535 , 505 , 477, 61 | 450 , 425 , 401 , 379 , 357 , 337 , 318 , 300 , 284 , 268 , 253 , 238, 62 | 225 , 212 , 200 , 189 , 179 , 169 , 159 , 150 , 142 , 134 , 126 , 119, 63 | 112 , 106 , 100 , 94 , 89 , 84 , 79 , 75 , 71 , 67 , 63 , 59 ], 64 | [1788, 1688, 1592, 1504, 1418, 1340, 1264, 1194, 1126, 1064, 1004, 948, 65 | 894 , 844 , 796 , 752 , 709 , 670 , 632 , 597 , 563 , 532 , 502 , 474, 66 | 447 , 422 , 398 , 376 , 355 , 335 , 316 , 298 , 282 , 266 , 251 , 237, 67 | 223 , 211 , 199 , 188 , 177 , 167 , 158 , 149 , 141 , 133 , 125 , 118, 68 | 111 , 105 , 99 , 94 , 88 , 83 , 79 , 74 , 70 , 66 , 62 , 59 ], 69 | [1774, 1676, 1582, 1492, 1408, 1330, 1256, 1184, 1118, 1056, 996 , 940, 70 | 887 , 838 , 791 , 746 , 704 , 665 , 628 , 592 , 559 , 528 , 498 , 470, 71 | 444 , 419 , 395 , 373 , 352 , 332 , 314 , 296 , 280 , 264 , 249 , 235, 72 | 222 , 209 , 198 , 187 , 176 , 166 , 157 , 148 , 140 , 132 , 125 , 118, 73 | 111 , 104 , 99 , 93 , 88 , 83 , 78 , 74 , 70 , 66 , 62 , 59 ], 74 | [1762, 1664, 1570, 1482, 1398, 1320, 1246, 1176, 1110, 1048, 988 , 934, 75 | 881 , 832 , 785 , 741 , 699 , 660 , 623 , 588 , 555 , 524 , 494 , 467, 76 | 441 , 416 , 392 , 370 , 350 , 330 , 312 , 294 , 278 , 262 , 247 , 233, 77 | 220 , 208 , 196 , 185 , 175 , 165 , 156 , 147 , 139 , 131 , 123 , 117, 78 | 110 , 104 , 98 , 92 , 87 , 82 , 78 , 73 , 69 , 65 , 61 , 58 ], 79 | [1750, 1652, 1558, 1472, 1388, 1310, 1238, 1168, 1102, 1040, 982 , 926, 80 | 875 , 826 , 779 , 736 , 694 , 655 , 619 , 584 , 551 , 520 , 491 , 463, 81 | 437 , 413 , 390 , 368 , 347 , 328 , 309 , 292 , 276 , 260 , 245 , 232, 82 | 219 , 206 , 195 , 184 , 174 , 164 , 155 , 146 , 138 , 130 , 123 , 116, 83 | 109 , 103 , 97 , 92 , 87 , 82 , 77 , 73 , 69 , 65 , 61 , 58 ], 84 | [1736, 1640, 1548, 1460, 1378, 1302, 1228, 1160, 1094, 1032, 974 , 920, 85 | 868 , 820 , 774 , 730 , 689 , 651 , 614 , 580 , 547 , 516 , 487 , 460, 86 | 434 , 410 , 387 , 365 , 345 , 325 , 307 , 290 , 274 , 258 , 244 , 230, 87 | 217 , 205 , 193 , 183 , 172 , 163 , 154 , 145 , 137 , 129 , 122 , 115, 88 | 108 , 102 , 96 , 91 , 86 , 81 , 77 , 72 , 68 , 64 , 61 , 57 ], 89 | [1724, 1628, 1536, 1450, 1368, 1292, 1220, 1150, 1086, 1026, 968 , 914, 90 | 862 , 814 , 768 , 725 , 684 , 646 , 610 , 575 , 543 , 513 , 484 , 457, 91 | 431 , 407 , 384 , 363 , 342 , 323 , 305 , 288 , 272 , 256 , 242 , 228, 92 | 216 , 203 , 192 , 181 , 171 , 161 , 152 , 144 , 136 , 128 , 121 , 114, 93 | 108 , 101 , 96 , 90 , 85 , 80 , 76 , 72 , 68 , 64 , 60 , 57 ]]; 94 | 95 | var SineTable = [ 96 | 0,24,49,74,97,120,141,161,180,197,212,224,235,244,250,253, 97 | 255,253,250,244,235,224,212,197,180,161,141,120,97,74,49, 98 | 24,0,-24,-49,-74,-97,-120,-141,-161,-180,-197,-212,-224, 99 | -235,-244,-250,-253,-255,-253,-250,-244,-235,-224,-212,-197, 100 | -180,-161,-141,-120,-97,-74,-49,-24 101 | ]; 102 | 103 | var ModPeriodToNoteNumber = {}; 104 | for (var i = 0; i < ModPeriodTable[0].length; i++) { 105 | ModPeriodToNoteNumber[ModPeriodTable[0][i]] = i; 106 | } 107 | 108 | function ModPlayer(mod, rate) { 109 | /* timing calculations */ 110 | var ticksPerSecond = 7093789.2; /* PAL frequency */ 111 | var ticksPerFrame; /* calculated by setBpm */ 112 | var ticksPerOutputSample = Math.round(ticksPerSecond / rate); 113 | var ticksSinceStartOfFrame = 0; 114 | 115 | function setBpm(bpm) { 116 | /* x beats per minute => x*4 rows per minute */ 117 | ticksPerFrame = Math.round(ticksPerSecond * 2.5/bpm); 118 | } 119 | setBpm(125); 120 | 121 | /* initial player state */ 122 | var framesPerRow = 6; 123 | var currentFrame = 0; 124 | var currentPattern; 125 | var currentPosition; 126 | var currentRow; 127 | var exLoop = false; //whether E6x looping is currently set 128 | var exLoopStart = 0; //loop point set up by E60 129 | var exLoopEnd = 0; //end of loop (where we hit a E6x cmd) for accurate counting 130 | var exLoopCount = 0; //loops remaining 131 | var doBreak = false; //Bxx, Dxx - jump to order and pattern break 132 | var breakPos = 0; 133 | var breakRow = 0; 134 | var delayRows = false; //EEx pattern delay. 135 | 136 | var channels = []; 137 | for (var chan = 0; chan < mod.channelCount; chan++) { 138 | channels[chan] = { 139 | playing: false, 140 | sample: mod.samples[0], 141 | finetune: 0, 142 | volume: 0, 143 | pan: 0x7F, //unimplemented 144 | volumeDelta: 0, 145 | periodDelta: 0, 146 | fineVolumeDelta: 0, 147 | finePeriodDelta: 0, 148 | tonePortaTarget: 0, //target for 3xx, 5xy as period value 149 | tonePortaDelta: 0, 150 | tonePortaVolStep: 0, //remember pitch slide step for when 5xx is used 151 | tonePortaActive: false, 152 | cut: false, //tick to cut at, or false if no cut 153 | delay: false, //tick to delay note until, or false if no delay 154 | arpeggioActive: false 155 | }; 156 | } 157 | 158 | function loadRow(rowNumber) { 159 | currentRow = rowNumber; 160 | currentFrame = 0; 161 | doBreak = false; 162 | breakPos = 0; 163 | breakRow = 0; 164 | 165 | for (var chan = 0; chan < mod.channelCount; chan++) { 166 | var channel = channels[chan]; 167 | var prevNote = channel.prevNote; 168 | var note = currentPattern[currentRow][chan]; 169 | if (channel.sampleNum == undefined) { 170 | channel.sampleNum = 0; 171 | } 172 | if (note.period != 0 || note.sample != 0) { 173 | channel.playing = true; 174 | channel.samplePosition = 0; 175 | channel.ticksSinceStartOfSample = 0; /* that's 'sample' as in 'individual volume reading' */ 176 | if (note.sample != 0) { 177 | channel.sample = mod.samples[note.sample - 1]; 178 | channel.sampleNum = note.sample - 1; 179 | channel.volume = channel.sample.volume; 180 | channel.finetune = channel.sample.finetune; 181 | } 182 | if (note.period != 0) { // && note.effect != 0x03 183 | //the note specified in a tone porta command is not actually played 184 | if (note.effect != 0x03) { 185 | channel.noteNumber = ModPeriodToNoteNumber[note.period]; 186 | channel.ticksPerSample = ModPeriodTable[channel.finetune][channel.noteNumber] * 2; 187 | } else { 188 | channel.noteNumber = ModPeriodToNoteNumber[prevNote.period] 189 | channel.ticksPerSample = ModPeriodTable[channel.finetune][channel.noteNumber] * 2; 190 | } 191 | } 192 | } 193 | channel.finePeriodDelta = 0; 194 | channel.fineVolumeDelta = 0; 195 | channel.cut = false; 196 | channel.delay = false; 197 | channel.retrigger = false; 198 | channel.tonePortaActive = false; 199 | if (note.effect != 0 || note.effectParameter != 0) { 200 | channel.volumeDelta = 0; /* new effects cancel volumeDelta */ 201 | channel.periodDelta = 0; /* new effects cancel periodDelta */ 202 | channel.arpeggioActive = false; 203 | switch (note.effect) { 204 | case 0x00: /* arpeggio: 0xy */ 205 | channel.arpeggioActive = true; 206 | channel.arpeggioNotes = [ 207 | channel.noteNumber, 208 | channel.noteNumber + (note.effectParameter >> 4), 209 | channel.noteNumber + (note.effectParameter & 0x0f) 210 | ] 211 | channel.arpeggioCounter = 0; 212 | break; 213 | case 0x01: /* pitch slide up - 1xx */ 214 | channel.periodDelta = -note.effectParameter; 215 | break; 216 | case 0x02: /* pitch slide down - 2xx */ 217 | channel.periodDelta = note.effectParameter; 218 | break; 219 | case 0x03: /* slide to note 3xy - */ 220 | channel.tonePortaActive = true; 221 | channel.tonePortaTarget = (note.period != 0) ? note.period : channel.tonePortaTarget; 222 | var dir = (channel.tonePortaTarget < prevNote.period) ? -1 : 1; 223 | channel.tonePortaDelta = (note.effectParameter * dir); 224 | channel.tonePortaVolStep = (note.effectParameter * dir); 225 | channel.tonePortaDir = dir; 226 | break; 227 | case 0x05: /* portamento to note with volume slide 5xy */ 228 | channel.tonePortaActive = true; 229 | if (note.effectParameter & 0xf0) { 230 | channel.volumeDelta = note.effectParameter >> 4; 231 | } else { 232 | channel.volumeDelta = -note.effectParameter; 233 | } 234 | channel.tonePortaDelta = channel.tonePortaVolStep; 235 | break; 236 | case 0x09: /* sample offset - 9xx */ 237 | channel.samplePosition = 256 * note.effectParameter; 238 | break; 239 | case 0x0A: /* volume slide - Axy */ 240 | if (note.effectParameter & 0xf0) { 241 | /* volume increase by x */ 242 | channel.volumeDelta = note.effectParameter >> 4; 243 | } else { 244 | /* volume decrease by y */ 245 | channel.volumeDelta = -note.effectParameter; 246 | } 247 | break; 248 | case 0x0B: /* jump to order */ 249 | doBreak = true; 250 | breakPos = note.effectParameter; 251 | breakRow = 0; 252 | break; 253 | case 0x0C: /* volume */ 254 | if (note.effectParameter > 64) { 255 | channel.volume = 64; 256 | } else { 257 | channel.volume = note.effectParameter; 258 | } 259 | break; 260 | case 0x0D: /* pattern break; jump to next pattern at specified row */ 261 | doBreak = true; 262 | breakPos = currentPosition + 1; 263 | //Row is written as DECIMAL so grab the high part as a single digit and do some math 264 | breakRow = ((note.effectParameter & 0xF0) >> 4) * 10 + (note.effectParameter & 0x0F); 265 | break; 266 | 267 | case 0x0E: 268 | switch (note.extEffect) { //yes we're doing nested switch 269 | case 0x01: /* fine pitch slide up - E1x */ 270 | channel.finePeriodDelta = -note.extEffectParameter; 271 | break; 272 | case 0x02: /* fine pitch slide down - E2x */ 273 | channel.finePeriodDelta = note.extEffectParameter; 274 | break; 275 | case 0x05: /* set finetune - E5x */ 276 | channel.finetune = note.extEffectParameter; 277 | break; 278 | case 0x09: /* retrigger sample - E9x */ 279 | channel.retrigger = note.extEffectParameter; 280 | break; 281 | case 0x0A: /* fine volume slide up - EAx */ 282 | channel.fineVolumeDelta = note.extEffectParameter; 283 | break; 284 | case 0x0B: /* fine volume slide down - EBx */ 285 | channel.fineVolumeDelta = -note.extEffectParameter; 286 | break; 287 | case 0x0C: /* note cut - ECx */ 288 | channel.cut = note.extEffectParameter; 289 | break; 290 | case 0x0D: /* note delay - EDx */ 291 | channel.delay = note.extEffectParameter; 292 | break; 293 | case 0x0E: /* pattern delay EEx */ 294 | delayRows = note.extEffectParameter; 295 | break; 296 | case 0x06: 297 | //set loop start with E60 298 | if (note.extEffectParameter == 0) { 299 | exLoopStart = currentRow; 300 | } else { 301 | //set loop end with E6x 302 | exLoopEnd = currentRow; 303 | //activate the loop only if it's new 304 | if (!exLoop) { 305 | exLoop = true; 306 | exLoopCount = note.extEffectParameter; 307 | } 308 | } 309 | break; 310 | } 311 | 312 | break; 313 | 314 | case 0x0F: /* tempo change. <=32 sets ticks/row, greater sets beats/min instead */ 315 | var newSpeed = (note.effectParameter == 0) ? 1 : note.effectParameter; /* 0 is treated as 1 */ 316 | if (newSpeed <= 32) { 317 | framesPerRow = newSpeed; 318 | } else { 319 | setBpm(newSpeed); 320 | } 321 | break; 322 | } 323 | } 324 | 325 | //for figuring out tone portamento effect 326 | if (note.period != 0) { channel.prevNote = note; } 327 | 328 | if (channel.tonePortaActive == false) { 329 | channel.tonePortaDelta = 0; 330 | channel.tonePortaTarget = 0; 331 | channel.tonePortaVolStep = 0; 332 | } 333 | } 334 | 335 | } 336 | 337 | function loadPattern(patternNumber) { 338 | var row = doBreak ? breakRow : 0; 339 | currentPattern = mod.patterns[patternNumber]; 340 | loadRow(row); 341 | } 342 | 343 | function loadPosition(positionNumber) { 344 | //Handle invalid position numbers that may be passed by invalid loop points 345 | positionNumber = (positionNumber > mod.positionCount - 1) ? 0 : positionNumber; 346 | currentPosition = positionNumber; 347 | loadPattern(mod.positions[currentPosition]); 348 | } 349 | 350 | loadPosition(0); 351 | 352 | function getNextPosition() { 353 | if (currentPosition + 1 >= mod.positionCount) { 354 | loadPosition(mod.positionLoopPoint); 355 | } else { 356 | loadPosition(currentPosition + 1); 357 | } 358 | } 359 | 360 | function getNextRow() { 361 | /* 362 | Determine where we're gonna go based on active effect. 363 | Either: 364 | break (jump to new pattern), 365 | do extended loop, 366 | advance normally 367 | */ 368 | if (doBreak) { 369 | //Dxx commands at the end of modules are fairly common for some reason 370 | //so make sure jumping past the end loops back to the start 371 | breakPos = (breakPos >= mod.positionCount) ? mod.positionLoopPoint : breakPos; 372 | loadPosition(breakPos); 373 | } else if (exLoop && currentRow == exLoopEnd && exLoopCount > 0) { 374 | //count down the loop and jump back 375 | loadRow(exLoopStart); 376 | exLoopCount--; 377 | } else { 378 | if (currentRow == 63) { 379 | getNextPosition(); 380 | } else { 381 | loadRow(currentRow + 1); 382 | } 383 | } 384 | 385 | if (exLoopCount < 0) { exLoop = false; } 386 | } 387 | 388 | function doFrame() { 389 | /* apply volume/pitch slide before fetching row, because the first frame of a row does NOT 390 | have the slide applied */ 391 | 392 | for (var chan = 0; chan < mod.channelCount; chan++) { 393 | var channel = channels[chan]; 394 | var finetune = channel.finetune; 395 | if (currentFrame == 0) { /* apply fine slides only once */ 396 | channel.ticksPerSample += channel.finePeriodDelta * 2; 397 | channel.volume += channel.fineVolumeDelta; 398 | } 399 | channel.volume += channel.volumeDelta; 400 | if (channel.volume > 64) { 401 | channel.volume = 64; 402 | } else if (channel.volume < 0) { 403 | channel.volume = 0; 404 | } 405 | if (channel.cut !== false && currentFrame >= channel.cut) { 406 | channel.volume = 0; 407 | } 408 | if (channel.delay !== false && currentFrame <= channel.delay) { 409 | channel.volume = 0; 410 | } 411 | if (channel.retrigger !== false) { 412 | //short-circuit prevents x mod 0 413 | if (channel.retrigger == 0 || currentFrame % channel.retrigger == 0) { 414 | channel.samplePosition = 0; 415 | } 416 | } 417 | channel.ticksPerSample += channel.periodDelta * 2; 418 | if (channel.tonePortaActive) { 419 | channel.ticksPerSample += channel.tonePortaDelta * 2; 420 | //don't slide below or above allowed note, depending on slide direction 421 | if (channel.tonePortaDir == 1 && channel.ticksPerSample > channel.tonePortaTarget * 2) { 422 | channel.ticksPerSample = channel.tonePortaTarget * 2; 423 | } else if (channel.tonePortaDir == -1 && channel.ticksPerSample < channel.tonePortaTarget * 2) { 424 | channel.ticksPerSample = channel.tonePortaTarget * 2; 425 | } 426 | } 427 | 428 | if (channel.ticksPerSample > 4096) { 429 | channel.ticksPerSample = 4096; 430 | } else if (channel.ticksPerSample < 96) { /* equivalent to period 48, a bit higher than the highest note */ 431 | channel.ticksPerSample = 96; 432 | } 433 | if (channel.arpeggioActive) { 434 | channel.arpeggioCounter++; 435 | var noteNumber = channel.arpeggioNotes[channel.arpeggioCounter % 3]; 436 | channel.ticksPerSample = ModPeriodTable[finetune][noteNumber] * 2; 437 | } 438 | } 439 | 440 | currentFrame++; 441 | if (currentFrame == framesPerRow) { 442 | currentFrame = 0; 443 | //Don't advance to reading more rows if pattern delay effect is active 444 | if (delayRows !== false) { 445 | delayRows--; 446 | if (delayRows < 0) { delayRows = false; } 447 | } else { 448 | getNextRow(); 449 | } 450 | } 451 | 452 | 453 | } 454 | 455 | this.getSamples = function(sampleCount) { 456 | var samplesLeft = []; 457 | var samplesRight = []; 458 | var i = 0; 459 | while (i < sampleCount) { 460 | ticksSinceStartOfFrame += ticksPerOutputSample; 461 | while (ticksSinceStartOfFrame >= ticksPerFrame) { 462 | doFrame(); 463 | ticksSinceStartOfFrame -= ticksPerFrame; 464 | } 465 | 466 | leftOutputLevel = 0; 467 | rightOutputLevel = 0; 468 | for (var chan = 0; chan < mod.channelCount; chan++) { 469 | var channel = channels[chan]; 470 | if (channel.playing) { 471 | channel.ticksSinceStartOfSample += ticksPerOutputSample; 472 | while (channel.ticksSinceStartOfSample >= channel.ticksPerSample) { 473 | channel.samplePosition++; 474 | if (channel.sample.repeatLength > 2 && channel.samplePosition >= channel.sample.repeatOffset + channel.sample.repeatLength) { 475 | channel.samplePosition = channel.sample.repeatOffset; 476 | } else if (channel.samplePosition >= channel.sample.length) { 477 | channel.playing = false; 478 | break; 479 | } else 480 | channel.ticksSinceStartOfSample -= channel.ticksPerSample; 481 | } 482 | if (channel.playing) { 483 | 484 | var rawVol = mod.sampleData[channel.sampleNum][channel.samplePosition]; 485 | var vol = (((rawVol + 128) & 0xff) - 128) * channel.volume; /* range (-128*64)..(127*64) */ 486 | if (chan & 3 == 0 || chan & 3 == 3) { /* hard panning(?): left, right, right, left */ 487 | leftOutputLevel += (vol + channel.pan) * 3; 488 | rightOutputLevel += (vol + 0xFF - channel.pan); 489 | } else { 490 | leftOutputLevel += (vol + 0xFF - channel.pan) 491 | rightOutputLevel += (vol + channel.pan) * 3; 492 | } 493 | /* range of outputlevels is 128*64*2*channelCount */ 494 | /* (well, it could be more for odd channel counts) */ 495 | } 496 | } 497 | } 498 | 499 | samplesLeft[i] = leftOutputLevel / (128 * 128 * mod.channelCount); 500 | samplesRight[i] = rightOutputLevel / (128 * 128 * mod.channelCount); 501 | i += 1; 502 | } 503 | 504 | return [samplesLeft, samplesRight]; 505 | } 506 | } 507 | -------------------------------------------------------------------------------- /src/xmfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | http://www.fileformat.info/format/xm/corion.htm 3 | 4 | Sample data is stored "Delta compressed like protracker" 5 | algorithm: http://www.fileformat.info/format/protracker/corion-algorithm.htm 6 | */ 7 | function XMFile(mod) { 8 | function trimNulls(str) { 9 | return str.replace(/\x00+$/, ''); 10 | } 11 | function getWord(str, pos) { 12 | //little-endian this time 13 | return (str.charCodeAt(pos)) + (str.charCodeAt(pos+1) << 8) 14 | } 15 | function getDword(str, pos) { 16 | var value = 17 | (str.charCodeAt(pos+3) << 24) + 18 | (str.charCodeAt(pos+2) << 16) + 19 | (str.charCodeAt(pos+1) << 8) + 20 | str.charCodeAt(pos); 21 | return value; 22 | } 23 | function getBytes(str, pos, len) { 24 | return (str.substr(pos, len)); 25 | } 26 | function getString(str, pos, len) { 27 | return trimNulls(getBytes(str, pos, len)); 28 | } 29 | function getArray(str, pos, len) { 30 | var s = getBytes(str, pos, len); 31 | var arr = Array(s.length); 32 | for (var i = 0; i < s.length; i++) { 33 | arr[i] = s.charCodeAt(i); 34 | } 35 | return arr; 36 | } 37 | 38 | //this.data = mod; 39 | this.samples = []; 40 | this.positions = []; 41 | this.patternCount = 0; 42 | this.patterns = []; 43 | this.instruments = []; 44 | this.speed = 6; 45 | this.bpm = 125; 46 | 47 | this.title = getString(mod, 0x11, 20); //0x11 Song name 48 | this.positionCount = getWord(mod, 0x40); //0x40 Song length in patterns 49 | this.positionLoopPoint = getWord(mod, 0x42); //0x42 Restart position 50 | this.channelCount = getWord(mod, 0x44); //0x44 Number of channels 51 | this.patternCount = getWord(mod, 0x46); //0x46 Number of patterns (0 - 255) 52 | this.instrumentCount = getWord(mod, 0x48); //0x48 Number of instruments (0 - 127) 53 | this.speed = getWord(mod, 0x4C); //0x4C Default ticks/row 54 | this.bpm = getWord(mod, 0x4E); //0x4E Default bpm 55 | 56 | //0x50 - pattern order table 57 | for (var i = 0; i < 256; i++) { 58 | this.positions[i] = mod.charCodeAt(0x50+i); 59 | } 60 | 61 | var patternOffset = 0x50 + 256; 62 | var track, packBit, rowCount, dataSize; 63 | for (var pat = 0; pat < 1; pat++) { 64 | var headerLength = getDword(mod, patternOffset); //Why? Isn't it always 9? 65 | rowCount = getWord(mod, patternOffset + 5); 66 | dataSize = getWord(mod, patternOffset + 7); 67 | this.patterns[pat] = { 68 | rowCount: rowCount, 69 | } 70 | 71 | //move pointer to first track of row then loop over each one 72 | patternOffset += 9; 73 | for (var row = 0; row < this.patterns[pat].rowCount; row++) { 74 | this.patterns[pat][row] = []; 75 | for (var chan = 0; chan < this.channelCount; chan++) { 76 | track = getArray(mod, patternOffset, 5); 77 | //console.log(track[0].toString(2), track); 78 | 79 | //If the most significant bit of a note is NOT set, then read data like normal 80 | //If it IS set, check the other bits and see what kind of data comes next 81 | //These are bitflags so 1 to 5 bytes may follow depending on how many are set 82 | // bit 0 set: Note byte follows 83 | // bit 1 set: Instrument byte follows 84 | // bit 2 set: Volume column byte follows 85 | // bit 3 set: Effect byte follows 86 | // bit 4 set: Effect data byte follows 87 | var packBit = track[0] & 0x80; 88 | var packFlags = track[0] & 0x1F; //00011111b 89 | var noteByte = 0, instrByte = 0, volByte = 0, effByte = 0, effParamByte = 0; 90 | 91 | if (packBit) { 92 | var o = 1; //offset 93 | //check each bit in order. If set, read byte and increment pointer 94 | if (packFlags & 0x01) { noteByte = track[o]; o++; } 95 | if (packFlags & 0x02) { instrByte = track[o]; o++; } 96 | if (packFlags & 0x04) { volByte = track[o]; o++; } 97 | if (packFlags & 0x08) { effByte = track[o]; o++; } 98 | if (packFlags & 0x10) { effParamByte = track[o]; o++; } 99 | patternOffset += o; 100 | } else { 101 | //no compression 102 | noteByte = track[0]; 103 | instrByte = track[1]; 104 | volByte = track[2]; 105 | effByte = track[3]; 106 | effParamByte = track[4]; 107 | patternOffset += 5; 108 | } 109 | 110 | this.patterns[pat][row][chan] = { 111 | note: noteByte, 112 | instrument: instrByte, 113 | volume: volByte, 114 | effect: effByte, 115 | effectParameter: effParamByte 116 | } 117 | } 118 | } 119 | } 120 | 121 | 122 | 123 | 124 | 125 | 126 | } -------------------------------------------------------------------------------- /testfiles.txt: -------------------------------------------------------------------------------- 1 | All mods are "4 channel M.K" (Protracker) format unless otherwise specified. Created in MilkyTracker 2 | Notes will generally continue to ring out because the volume effect is not used. (Strict test of 1 effect at a time!) 3 | This is the correct behavior. 4 | 5 | 6 | basic.mod 7 | 8 | Simple playback. C major scale with 1 sine sample. 9 | 10 | CORRECT: Plays C scale for one octave 11 | WRONG: Anything else 12 | 13 | break.mod 14 | 15 | Test of Dxx pattern break. 16 | Frame 1 C major (forward) on row 0x0A 17 | Frame 2 C major (backward) 18 | 19 | CORRECT: Immediately plays C scale BACKWARD 20 | WRONG: Plays C forward first 21 | 22 | break_far.mod 23 | 24 | Dxx test. Ensures that player processes offset as DECIMAL not hex. 25 | Frame 1 C major (forward) on row 2 after B32 jump 26 | Frame 2 C major (backward) on row 0x20 27 | 28 | CORRECT: Immediately plays C scale BACKWARD 29 | WRONG: Plays C forward first 30 | 31 | jump.mod 32 | 33 | Tests Bxx "jump to order" command (specific place in pattern table) 34 | 35 | CORRECT: Immediately plays C scale BACKWARD. 36 | WRONG: Plays C forward first 37 | 38 | fineporta.mod 39 | 40 | Tests E1x, fine pitch slide up. 41 | Alternates between normal slide (1xx) and fine slide. Both should sound identical 42 | 43 | CORRECT: Slides sound identical, and a fixed tone rings out after 6 repeats 44 | WRONG: Slides sound different 45 | Pitch continues to slide up after 6 repeats (applying to incorrect rows) 46 | One or the other sounds flat (slide not working) 47 | 48 | ex_loop.mod 49 | 50 | Test E6x, loop points 51 | 52 | C major scale with loop point half way through 53 | 54 | CORRECT: Plays first 4 notes twice followed by whole scale 55 | WRONG: Just plays whole scale 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /tests/basic.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BillyWM/jsmodplayer/b33600b0e55a7ba33d2bef9ab6f0767097c2a081/tests/basic.mod -------------------------------------------------------------------------------- /tests/break.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BillyWM/jsmodplayer/b33600b0e55a7ba33d2bef9ab6f0767097c2a081/tests/break.mod -------------------------------------------------------------------------------- /tests/break_far.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BillyWM/jsmodplayer/b33600b0e55a7ba33d2bef9ab6f0767097c2a081/tests/break_far.mod -------------------------------------------------------------------------------- /tests/cut.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BillyWM/jsmodplayer/b33600b0e55a7ba33d2bef9ab6f0767097c2a081/tests/cut.mod -------------------------------------------------------------------------------- /tests/ex_loop.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BillyWM/jsmodplayer/b33600b0e55a7ba33d2bef9ab6f0767097c2a081/tests/ex_loop.mod -------------------------------------------------------------------------------- /tests/fineporta.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BillyWM/jsmodplayer/b33600b0e55a7ba33d2bef9ab6f0767097c2a081/tests/fineporta.mod -------------------------------------------------------------------------------- /tests/jump.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BillyWM/jsmodplayer/b33600b0e55a7ba33d2bef9ab6f0767097c2a081/tests/jump.mod --------------------------------------------------------------------------------