Index: Rakefile ================================================================== --- Rakefile +++ Rakefile @@ -55,10 +55,11 @@ else puts "Unknown optMode: #{args[:optMode]}" exit 1 end + cmdLine += " -Dstrict_multi_assign -Dno_number_autocast" cmdLine += " #{mainSrc}" sh cmdLine end file VGMINFO_BIN, [:optMode] => SOURCES + VGMINFO_SRC + [SHARD, SHARD_LOCK] do |t, args| Index: src/yunosynth/chips/chip-nes.cr ================================================================== --- src/yunosynth/chips/chip-nes.cr +++ src/yunosynth/chips/chip-nes.cr @@ -39,11 +39,11 @@ @dmc : NesDmcNsfPlay? @fds : NesFdsNsfPlay? @memory : Array(UInt8) = [] of UInt8 @hasFds : Bool = false property options : UInt16 = 0x8000_u16 - @buffer : Array(Int32) = [0, 0, 0, ] + @buffer : Array(Int32) = [0, 0, 0, 0] def initialize(chipNum : Int32, absChipCount : Int32, vgm : VgmFile, playbackSampleRate : UInt32, newSamplingMode : UInt8, newPlayerSampleRate : UInt32, *, emuCore : Symbol? = nil, flags : ChipFlags? = nil) @clockFromHeader = vgm.getAlternateChipClock(chipNum, Yuno::ChipType::NesApu) || vgm.header.nesApuClock ADDED src/yunosynth/chips/chip-ym2413.cr Index: src/yunosynth/chips/chip-ym2413.cr ================================================================== --- /dev/null +++ src/yunosynth/chips/chip-ym2413.cr @@ -0,0 +1,216 @@ +#### YunoSynth +#### Copyright (C) 2023-2024 Remilia Scarlet +#### Portions based on VGMPlay, Copyright (C) Valley Bell +#### +#### This program is free software: you can redistribute it and/or modify it +#### under the terms of the GNU Affero General Public License as published by +#### the Free Software Foundation, either version 3 of the License, or (at your +#### option) any later version. +#### +#### This program is distributed in the hope that it will be useful, but WITHOUT +#### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +#### FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public +#### License for more details. +#### +#### You should have received a copy of the GNU Affero General Public License +#### along with this program. If not, see . +require "./emu-ym2413-mame" + +#### +#### Yamaha YM2413 sound chip emulator interface +#### + +module Yuno::Chips + # YM2413 sound chip emulator. + class YM2413 < Yuno::AbstractChip + CHIP_ID = 0x01_u32 + + enum Core + Emu2413 + + @[AlwaysInline] + def symbol : Symbol + case self + in .emu2413? then :emu2413 + end + end + end + + # :nodoc: + VRC7_INST = [ + # VRC7 VOICE + # Dumped via VRC7 debug mode by Nuke.YKT + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0x21, 0x05, 0x06, 0xE8, 0x81, 0x42, 0x27, + 0x13, 0x41, 0x14, 0x0D, 0xD8, 0xF6, 0x23, 0x12, + 0x11, 0x11, 0x08, 0x08, 0xFA, 0xB2, 0x20, 0x12, + 0x31, 0x61, 0x0C, 0x07, 0xA8, 0x64, 0x61, 0x27, + 0x32, 0x21, 0x1E, 0x06, 0xE1, 0x76, 0x01, 0x28, + 0x02, 0x01, 0x06, 0x00, 0xA3, 0xE2, 0xF4, 0xF4, + 0x21, 0x61, 0x1D, 0x07, 0x82, 0x81, 0x11, 0x07, + 0x23, 0x21, 0x22, 0x17, 0xA2, 0x72, 0x01, 0x17, + 0x35, 0x11, 0x25, 0x00, 0x40, 0x73, 0x72, 0x01, + 0xB5, 0x01, 0x0F, 0x0F, 0xA8, 0xA5, 0x51, 0x02, + 0x17, 0xC1, 0x24, 0x07, 0xF8, 0xF8, 0x22, 0x12, + 0x71, 0x23, 0x11, 0x06, 0x65, 0x74, 0x18, 0x16, + 0x01, 0x02, 0xD3, 0x05, 0xC9, 0x95, 0x03, 0x02, + 0x61, 0x63, 0x0C, 0x00, 0x94, 0xC0, 0x33, 0xF6, + 0x21, 0x72, 0x0D, 0x00, 0xC1, 0xD5, 0x56, 0x06, + 0x01, 0x01, 0x18, 0x0F, 0xDF, 0xF8, 0x6A, 0x6D, + 0x01, 0x01, 0x00, 0x00, 0xC8, 0xD8, 0xA7, 0x68, + 0x05, 0x01, 0x00, 0x00, 0xF8, 0xAA, 0x59, 0x55, + ] of UInt8 + + @clockFromHeader : UInt32 = 0 + @chip : YM2413Emu2413|Nil = nil + @samplingMode : UInt8 = 0 + @chipSampleRate : UInt32 = 0 + @lastReg : UInt8 = 0 + @subType : UInt8 = 0 + + def initialize(chipNum : Int32, absChipCount : Int32, vgm : VgmFile, playbackSampleRate : UInt32, + newSamplingMode : UInt8, newPlayerSampleRate : UInt32, *, emuCore : Core|Symbol|Nil = nil, + flags : ChipFlags? = nil) + @clockFromHeader = vgm.getAlternateChipClock(chipNum, Yuno::ChipType::Ym2413) || vgm.header.ym2413Clock + @clockFromHeader &= 0xBFFFFFFF + @subType = Yuno.bitflag?(@clockFromHeader, 0x80000000) ? 0x01_u8 : 0x00_u8 + @playerSampleRate = newPlayerSampleRate + chipCount = (vgm.header.ym2413Clock & 0x40000000) != 0 ? 2 : 1 + initFields(vgm, emuCore, playbackSampleRate, newSamplingMode, chipCount) + end + + protected def initialize + end + + protected def initFields(vgm : VgmFile, emuCore : Core|Symbol|Nil, playbackSampleRate : UInt32, + newSamplingMode : UInt8, chipCount : Int32) : Nil + @volume = vgm.getChipVolume(self, ChipType::Ym2413, chipCount) + @samplingMode = newSamplingMode + @core = case emuCore + when Core then emuCore.symbol + when Symbol then emuCore + else YM2413.defaultEmuCore + end + case @core + when :emu2413 + @chipSampleRate = @clockFromHeader <= 0 ? playbackSampleRate : @clockFromHeader + else raise YunoError.new("Unsupported emulation core for YM2413: #{@core}") + end + end + + def type : ChipType + ChipType::Ym2413 + end + + def name : String + case @subType + when 1 then "Konami VRC7" + else "Yamaha YM2413" + end + end + + def shortName : String + case @subType + when 1 then "VRC7" + else "YM2413" + end + end + + def id : UInt32 + CHIP_ID + end + + def emuCore : Symbol + @core + end + + def self.defaultEmuCore : Symbol + :emu2413 + end + + def start(chipIndex : UInt8, clock : UInt32, flags : ChipFlags? = nil) : UInt32 + mode = (clock & 0x80000000) >> 31 + clock &= 0x7FFFFFFF + + # Update sample rate + @sampleRate = clock.tdiv(72) + if (@samplingMode == 0x01 && @sampleRate < @chipSampleRate) || @samplingMode == 0x02 + @sampleRate = @chipSampleRate + end + + # Note: VRC7 instruments are set in #reset if necessary. + case @core + when :emu2413 + @chip = YM2413Emu2413.new(clock, @sampleRate) + @chip.as(YM2413Emu2413).setChipType(mode) + if mode != 0 + @chip.as(YM2413Emu2413).patch = VRC7_INST + end + @sampleRate + else raise YunoError.new("Unsupported emulation core for YM2413: #{@core}") + end + end + + def getClock : UInt32 + @clockFromHeader + end + + def getStartFlags(vgm : VgmFile) : ChipFlags? + nil + end + + def update(outputs : OutputBuffers, samples : Int) : Nil + ch = @chip.not_nil! + case ch + when YM2413Emu2413 then ch.update(outputs, samples.to_u32!) + end + end + + def reset(chipIndex : UInt8) : Nil + ch = @chip.not_nil! + case ch + when YM2413Emu2413 then ch.reset + else raise "Unexpected YM2413 type in YM2413#reset" + end + end + + def read(chipIndex : UInt8, offset : Int) : UInt8|UInt16|UInt32 + 0u8 + end + + def write(chipIndex : UInt8, offset : Int, data : Int, port : UInt8 = 0) : Nil + ch = @chip.not_nil! + case ch + when YM2413Emu2413 then ch.writeIO((offset & 1).to_u32!, data.to_u8!) + else raise "Unexpected YM2413 type in YM2413#write" + end + end + + def writeDac(port : Int, command : Int, data : Int) : Nil + write(0, 0x00, command) + write(0, 0x01, data) + end + + def setMuteMask(chipIndex : UInt8, mask : UInt32) : Nil + ch = @chip.not_nil! + case ch + when YM2413Emu2413 then ch.mask = mask + else raise "Unexpected YM2413 type in YM2413#setMuteMask" + end + end + + def getVolModifier : UInt32 + @volume.to_u32!.tdiv(2) + end + + def baseVolume : UInt16 + 0x200_u16 + end + + # The internal interface for the chip. + @[AlwaysInline] + def chip : AbstractEmulator + @chip.not_nil! + end + end +end ADDED src/yunosynth/chips/emu-ym2413-mame.cr Index: src/yunosynth/chips/emu-ym2413-mame.cr ================================================================== --- /dev/null +++ src/yunosynth/chips/emu-ym2413-mame.cr @@ -0,0 +1,1529 @@ +#### emu2413 v1.5.2 +#### https://github.com/digital-sound-antiques/emu2413 +#### Copyright (C) 2020 Mitsutaka Okazaki +#### Crystal port Copyright (C) 2024 Remilia Scarlet +#### MIT License +#### +#### This source refers to the following documents. The author would like to thank all the authors who have +#### contributed to the writing of them. +#### - [YM2413 notes](http://www.smspower.org/Development/YM2413) by andete +#### - ymf262.c by Jarek Burczynski +#### - [VRC7 presets](https://siliconpr0n.org/archive/doku.php?id=vendor:yamaha:opl2#opll_vrc7_patch_format) by Nuke.YKT +#### - YMF281B presets by Chabin + +#### +#### Yamaha YM2413 sound chip emulator internal interface. +#### + +module Yuno::Chips + class YM2413 < AbstractChip + private class YM2413Emu2413 < Yuno::AbstractEmulator + ### + ### Constants and Macros + ### + + OPLL_2413_TONE = 0 + OPLL_VRC7_TONE = 1 + OPLL_281B_TONE = 2 + + OPLL_TONE_NUM = 3 + + private macro maskCh(x) + 1 << {{x}} + end + + MASK_HH = 1 << 9 + MASK_CYM = 1 << 10 + MASK_TOM = 1 << 11 + MASK_SD = 1 << 12 + MASK_BD = 1 << 13 + MASK_RHYTHM = (MASK_HH | MASK_CYM | MASK_TOM | MASK_SD | MASK_BD) + + DEFAULT_INST = [ + [ + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, # 0: Original + 0x71,0x61,0x1E,0x17,0xD0,0x78,0x00,0x17, # 1: Violin + 0x13,0x41,0x1A,0x0D,0xD8,0xF7,0x23,0x13, # 2: Guitar + 0x13,0x01,0x99,0x00,0xF2,0xC4,0x21,0x23, # 3: Piano + 0x11,0x61,0x0E,0x07,0x8D,0x64,0x70,0x27, # 4: Flute + 0x32,0x21,0x1E,0x06,0xE1,0x76,0x01,0x28, # 5: Clarinet + 0x31,0x22,0x16,0x05,0xE0,0x71,0x00,0x18, # 6: Oboe + 0x21,0x61,0x1D,0x07,0x82,0x81,0x11,0x07, # 7: Trumpet + 0x33,0x21,0x2D,0x13,0xB0,0x70,0x00,0x07, # 8: Organ + 0x61,0x61,0x1B,0x06,0x64,0x65,0x10,0x17, # 9: Horn + 0x41,0x61,0x0B,0x18,0x85,0xF0,0x81,0x07, # A: Synthesizer + 0x33,0x01,0x83,0x11,0xEA,0xEF,0x10,0x04, # B: Harpsichord + 0x17,0xC1,0x24,0x07,0xF8,0xF8,0x22,0x12, # C: Vibraphone + 0x61,0x50,0x0C,0x05,0xD2,0xF5,0x40,0x42, # D: Synthsizer Bass + 0x01,0x01,0x55,0x03,0xE9,0x90,0x03,0x02, # E: Acoustic Bass + 0x41,0x41,0x89,0x03,0xF1,0xE4,0xC0,0x13, # F: Electric Guitar + 0x01,0x01,0x18,0x0F,0xDF,0xF8,0x6A,0x6D, # R: Bass Drum (from VRC7) + 0x01,0x01,0x00,0x00,0xC8,0xD8,0xA7,0x68, # R: High-Hat(M) / Snare Drum(C) (from VRC7) + 0x05,0x01,0x00,0x00,0xF8,0xAA,0x59,0x55 # R: Tom-tom(M) / Top Cymbal(C) (from VRC7) + ] of UInt8, + [ + # VRC7 presets from Nuke.YKT + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x03,0x21,0x05,0x06,0xE8,0x81,0x42,0x27, + 0x13,0x41,0x14,0x0D,0xD8,0xF6,0x23,0x12, + 0x11,0x11,0x08,0x08,0xFA,0xB2,0x20,0x12, + 0x31,0x61,0x0C,0x07,0xA8,0x64,0x61,0x27, + 0x32,0x21,0x1E,0x06,0xE1,0x76,0x01,0x28, + 0x02,0x01,0x06,0x00,0xA3,0xE2,0xF4,0xF4, + 0x21,0x61,0x1D,0x07,0x82,0x81,0x11,0x07, + 0x23,0x21,0x22,0x17,0xA2,0x72,0x01,0x17, + 0x35,0x11,0x25,0x00,0x40,0x73,0x72,0x01, + 0xB5,0x01,0x0F,0x0F,0xA8,0xA5,0x51,0x02, + 0x17,0xC1,0x24,0x07,0xF8,0xF8,0x22,0x12, + 0x71,0x23,0x11,0x06,0x65,0x74,0x18,0x16, + 0x01,0x02,0xD3,0x05,0xC9,0x95,0x03,0x02, + 0x61,0x63,0x0C,0x00,0x94,0xC0,0x33,0xF6, + 0x21,0x72,0x0D,0x00,0xC1,0xD5,0x56,0x06, + 0x01,0x01,0x18,0x0F,0xDF,0xF8,0x6A,0x6D, + 0x01,0x01,0x00,0x00,0xC8,0xD8,0xA7,0x68, + 0x05,0x01,0x00,0x00,0xF8,0xAA,0x59,0x55 + ] of UInt8, + [ + # YMF281B presets by Chabin + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x62,0x21,0x1A,0x07,0xF0,0x6F,0x00,0x16, + 0x00,0x10,0x44,0x02,0xF6,0xF4,0x54,0x23, + 0x03,0x01,0x97,0x04,0xF3,0xF3,0x13,0xF3, + 0x01,0x61,0x0A,0x0F,0xFA,0x64,0x70,0x17, + 0x22,0x21,0x1E,0x06,0xF0,0x76,0x00,0x28, + 0x00,0x61,0x8A,0x0E,0xC0,0x61,0x00,0x07, + 0x21,0x61,0x1B,0x07,0x84,0x80,0x17,0x17, + 0x37,0x32,0xC9,0x01,0x66,0x64,0x40,0x28, + 0x01,0x21,0x06,0x03,0xA5,0x71,0x51,0x07, + 0x06,0x11,0x5E,0x07,0xF3,0xF2,0xF6,0x11, + 0x00,0x20,0x18,0x06,0xF5,0xF3,0x20,0x26, + 0x97,0x41,0x20,0x07,0xFF,0xF4,0x22,0x22, + 0x65,0x61,0x15,0x00,0xF7,0xF3,0x16,0xF4, + 0x01,0x31,0x0E,0x07,0xFA,0xF3,0xFF,0xFF, + 0x48,0x61,0x09,0x07,0xF1,0x94,0xF0,0xF5, + 0x07,0x21,0x14,0x00,0xEE,0xF8,0xFF,0xF8, + 0x01,0x31,0x00,0x00,0xF8,0xF7,0xF8,0xF7, + 0x25,0x11,0x00,0x00,0xF8,0xFA,0xF8,0x55 + ] of UInt8 + ] + + ## + ## Phase increment counter + ## + DP_BITS = 19 + DP_WIDTH = 1 << DP_BITS + DP_BASE_BITS = DP_BITS - PG_BITS + + ## + ## Dynamic range of envelope output + ## + EG_STEP = 0.375 + EG_BITS = 7 + EG_MUTE = ((1 << EG_BITS) - 1).to_u32! + EG_MAX = EG_MUTE - 3 + + ## + ## Dynamic range of total level + ## + TL_STEP = 0.75 + TL_BITS = 6 + + ## + ## Dynamic range of sustain level + ## + SL_STEP = 3.0 + SL_BITS = 4 + + ## + ## Damper speed before key-on. key-scale affects. + ## + DAMPER_RATE = 12_u32 + + private macro tl2eg(d) + {{d}} << 1 + end + + ## + ## Sine table + ## + PG_BITS = 10 # 2^10 = 1024 length sine table + PG_WIDTH = 1 << PG_BITS + + # exp_table[x] = round((exp2((double)x / 256.0) - 1) * 1024) + EXP_TABLE = [ + 0, 3, 6, 8, 11, 14, 17, 20, 22, 25, 28, 31, 34, 37, 40, 42, + 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, + 93, 96, 99, 102, 105, 108, 111, 114, 117, 120, 123, 126, 130, 133, 136, 139, + 142, 145, 148, 152, 155, 158, 161, 164, 168, 171, 174, 177, 181, 184, 187, 190, + 194, 197, 200, 204, 207, 210, 214, 217, 220, 224, 227, 231, 234, 237, 241, 244, + 248, 251, 255, 258, 262, 265, 268, 272, 276, 279, 283, 286, 290, 293, 297, 300, + 304, 308, 311, 315, 318, 322, 326, 329, 333, 337, 340, 344, 348, 352, 355, 359, + 363, 367, 370, 374, 378, 382, 385, 389, 393, 397, 401, 405, 409, 412, 416, 420, + 424, 428, 432, 436, 440, 444, 448, 452, 456, 460, 464, 468, 472, 476, 480, 484, + 488, 492, 496, 501, 505, 509, 513, 517, 521, 526, 530, 534, 538, 542, 547, 551, + 555, 560, 564, 568, 572, 577, 581, 585, 590, 594, 599, 603, 607, 612, 616, 621, + 625, 630, 634, 639, 643, 648, 652, 657, 661, 666, 670, 675, 680, 684, 689, 693, + 698, 703, 708, 712, 717, 722, 726, 731, 736, 741, 745, 750, 755, 760, 765, 770, + 774, 779, 784, 789, 794, 799, 804, 809, 814, 819, 824, 829, 834, 839, 844, 849, + 854, 859, 864, 869, 874, 880, 885, 890, 895, 900, 906, 911, 916, 921, 927, 932, + 937, 942, 948, 953, 959, 964, 969, 975, 980, 986, 991, 996, 1002, 1007, 1013, 1018 + ] of UInt16 + + # fullsin_table[x] = round(-log2(sin((x + 0.5) * PI / (PG_WIDTH / 4) / 2)) * 256) + @@fullsinTable : Array(UInt16) = [ + 2137, 1731, 1543, 1419, 1326, 1252, 1190, 1137, 1091, 1050, 1013, 979, 949, 920, 894, 869, + 846, 825, 804, 785, 767, 749, 732, 717, 701, 687, 672, 659, 646, 633, 621, 609, + 598, 587, 576, 566, 556, 546, 536, 527, 518, 509, 501, 492, 484, 476, 468, 461, + 453, 446, 439, 432, 425, 418, 411, 405, 399, 392, 386, 380, 375, 369, 363, 358, + 352, 347, 341, 336, 331, 326, 321, 316, 311, 307, 302, 297, 293, 289, 284, 280, + 276, 271, 267, 263, 259, 255, 251, 248, 244, 240, 236, 233, 229, 226, 222, 219, + 215, 212, 209, 205, 202, 199, 196, 193, 190, 187, 184, 181, 178, 175, 172, 169, + 167, 164, 161, 159, 156, 153, 151, 148, 146, 143, 141, 138, 136, 134, 131, 129, + 127, 125, 122, 120, 118, 116, 114, 112, 110, 108, 106, 104, 102, 100, 98, 96, + 94, 92, 91, 89, 87, 85, 83, 82, 80, 78, 77, 75, 74, 72, 70, 69, + 67, 66, 64, 63, 62, 60, 59, 57, 56, 55, 53, 52, 51, 49, 48, 47, + 46, 45, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, + 29, 28, 27, 26, 25, 24, 23, 23, 22, 21, 20, 20, 19, 18, 17, 17, + 16, 15, 15, 14, 13, 13, 12, 12, 11, 10, 10, 9, 9, 8, 8, 7, + 7, 7, 6, 6, 5, 5, 5, 4, 4, 4, 3, 3, 3, 2, 2, 2, + 2, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0 + ] of UInt16 + + @@halfsinTable : Array(UInt16) = Array(UInt16).new(PG_WIDTH, 0u16) + protected class_getter waveTableMap : Array(Array(UInt16)) = [@@fullsinTable, @@halfsinTable] + + # Pitch modulator + # + # Offset to fnum, rough approximation of 14 cents depth. + PM_TABLE = [ + [0, 0, 0, 0, 0, 0, 0, 0] of Int8, #fnum = 000xxxxxx + [0, 0, 1, 0, 0, 0, -1, 0] of Int8, #fnum = 001xxxxxx + [0, 1, 2, 1, 0, -1, -2, -1] of Int8, #fnum = 010xxxxxx + [0, 1, 3, 1, 0, -1, -3, -1] of Int8, #fnum = 011xxxxxx + [0, 2, 4, 2, 0, -2, -4, -2] of Int8, #fnum = 100xxxxxx + [0, 2, 5, 2, 0, -2, -5, -2] of Int8, #fnum = 101xxxxxx + [0, 3, 6, 3, 0, -3, -6, -3] of Int8, #fnum = 110xxxxxx + [0, 3, 7, 3, 0, -3, -7, -3] of Int8 #fnum = 111xxxxxx + ] + + # Amplitude LFO table + # + # The following envelop pattern is verified on real YM2413. Each element + # repeats 64 cycles. + AM_TABLE = [ + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, + 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, + 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, + 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, + 12, 12, 12, 12, 12, 12, 12, 12, + 13, 13, 13, + 12, 12, 12, 12, 12, 12, 12, 12, + 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 10, 10, 10, 10, 10, 10, + 9, 9, 9, 9, 9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 8, 8, + 7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, + 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, + 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0 + ] of UInt8 + + # Envelope decay increment step table, based on andete's research. + EG_STEP_TABLES = [ + [0, 1, 0, 1, 0, 1, 0, 1] of UInt8, + [0, 1, 0, 1, 1, 1, 0, 1] of UInt8, + [0, 1, 1, 1, 0, 1, 1, 1] of UInt8, + [0, 1, 1, 1, 1, 1, 1, 1] of UInt8 + ] + + ATTACK = 1u8 + DECAY = 2u8 + SUSTAIN = 3u8 + RELEASE = 4u8 + DAMP = 5u8 + UNKNOWN = 6u8 + + ML_TABLE = [ + 1u32, 1u32 * 2, 2u32 * 2, 3u32 * 2, 4u32 * 2, 5u32 * 2, 6u32 * 2, 7u32 * 2, + 8u32 * 2, 9u32 * 2, 10u32 * 2, 10u32 * 2, 12u32 * 2, 12u32 * 2, 15u32 * 2, 15u32 * 2 + ] of UInt32 + + KL_TABLE = [ + 0.000 * 2, 9.000 * 2, 12.000 * 2, 13.875 * 2, 15.000 * 2, 16.125 * 2, + 16.875 * 2, 17.625 * 2, 18.000 * 2, 18.750 * 2, 19.125 * 2, 19.500 * 2, + 19.875 * 2, 20.250 * 2, 20.625 * 2, 21.000 * 2 + ] + + protected class_getter tllTable : Array(Array(Array(UInt32))) = [[[] of UInt32]] + protected class_getter rksTable : Array(Array(Int32)) = [[] of Int32] + protected class_getter defaultPatch : Array(Array(Patch)) = [[] of Patch] + + @@tablesMade : Bool = false + + UPDATE_WS = 1 + UPDATE_TLL = 2 + UPDATE_RKS = 4 + UPDATE_EG = 8 + UPDATE_ALL = 255 + + PANNING_RANGE = 512 + SQRT2 = Math.sqrt(2) + + SLOT_BD1 = 12 + SLOT_BD2 = 13 + SLOT_HH = 14 + SLOT_SD = 15 + SLOT_TOM = 16 + SLOT_CYM = 17 + + ### + ### Private Classes + ### + + private class Patch + property tl : UInt32 = 0u32 + property fb : UInt32 = 0u32 + property eg : UInt32 = 0u32 + property ml : UInt32 = 0u32 + property ar : UInt32 = 0u32 + property dr : UInt32 = 0u32 + property sl : UInt32 = 0u32 + property rr : UInt32 = 0u32 + property kr : UInt32 = 0u32 + property kl : UInt32 = 0u32 + property am : UInt32 = 0u32 + property pm : UInt32 = 0u32 + property ws : UInt32 = 0u32 + + def initialize + end + + def self.fromDump(dump : Pointer(UInt8)) : Array(Patch) + patch = [Patch.new, Patch.new] + patch[0].am = (dump[0].to_u32! >> 7) & 1 + patch[1].am = (dump[1].to_u32! >> 7) & 1 + patch[0].pm = (dump[0].to_u32! >> 6) & 1 + patch[1].pm = (dump[1].to_u32! >> 6) & 1 + patch[0].eg = (dump[0].to_u32! >> 5) & 1 + patch[1].eg = (dump[1].to_u32! >> 5) & 1 + patch[0].kr = (dump[0].to_u32! >> 4) & 1 + patch[1].kr = (dump[1].to_u32! >> 4) & 1 + patch[0].ml = (dump[0].to_u32!) & 15 + patch[1].ml = (dump[1].to_u32!) & 15 + patch[0].kl = (dump[2].to_u32! >> 6) & 3 + patch[1].kl = (dump[3].to_u32! >> 6) & 3 + patch[0].tl = (dump[2].to_u32!) & 63 + patch[1].tl = 0u32 + patch[0].fb = (dump[3].to_u32!) & 7 + patch[1].fb = 0u32 + patch[0].ws = (dump[3].to_u32! >> 3) & 1 + patch[1].ws = (dump[3].to_u32! >> 4) & 1 + patch[0].ar = (dump[4].to_u32! >> 4) & 15 + patch[1].ar = (dump[5].to_u32! >> 4) & 15 + patch[0].dr = (dump[4].to_u32!) & 15 + patch[1].dr = (dump[5].to_u32!) & 15 + patch[0].sl = (dump[6].to_u32! >> 4) & 15 + patch[1].sl = (dump[7].to_u32! >> 4) & 15 + patch[0].rr = (dump[6].to_u32!) & 15 + patch[1].rr = (dump[7].to_u32!) & 15 + patch + end + + def self.getDefaultPatch(typ : Int32, num : Int32) : Array(Patch) + fromDump(DEFAULT_INST[typ].to_unsafe + (num * 8)) + end + + def copy(other : Patch) : Nil + @tl = other.tl + @fb = other.fb + @eg = other.eg + @ml = other.ml + @ar = other.ar + @dr = other.dr + @sl = other.sl + @rr = other.rr + @kr = other.kr + @kl = other.kl + @am = other.am + @pm = other.pm + @ws = other.ws + end + + def toDump(dump : Pointer(UInt8)) : Nil + dump[0] = ((patch[0].am << 7) + (patch[0].pm << 6) + (patch[0].eg << 5) + + (patch[0].kr << 4) + patch[0].ml).to_u8! + dump[1] = ((patch[1].am << 7) + (patch[1].pm << 6) + (patch[1].eg << 5) + + (patch[1].kr << 4) + patch[1].ml).to_u8! + dump[2] = ((patch[0].kl << 6) + patch[0].tl).to_u8! + dump[3] = ((patch[1].kl << 6) + (patch[1].ws << 4) + (patch[0].ws << 3) + patch[0].fb).to_u8! + dump[4] = ((patch[0].ar << 4) + patch[0].dr).to_u8! + dump[5] = ((patch[1].ar << 4) + patch[1].dr).to_u8! + dump[6] = ((patch[0].sl << 4) + patch[0].rr).to_u8! + dump[7] = ((patch[1].sl << 4) + patch[1].rr).to_u8! + end + end + + private class Slot + property number : UInt8 = 0u8 + + # type flags: + # 000000SM + # |+-- M: 0:modulator 1:carrier + # +--- S: 0:normal 1:single slot mode (sd, tom, hh or cym) + property type : UInt8 = 0u8 + + # Voice parameter + property patch : Patch = Patch.new + + # Slot output + property output : Array(Int32) = [0, 0] # Output value, latest and previous. + + ## + ## Phase generator (pg) + ## + property waveTable : Array(UInt16) = [] of UInt16 # Wave table + property pgPhase : UInt32 = 0u32 # pg phase + property pgOut : UInt32 = 0u32 # pg output, as index of wave table + property pgKeep : UInt8 = 0u8 # if 1, @phPhase is preserved when key-on + property blkFnum : UInt16 = 0u16 # (block << 9) | f-number + property fnum : UInt16 = 0u16 # f-number (9 bits) + property blk : UInt8 = 0u8 # block (3 bits) + + ## + ## Envelope generator (eg) + ## + property eg_state : UInt8 = 0u8 # current state + property volume : Int32 = 0 # current volume + property keyFlag : UInt8 = 0u8 # key-on flag 1:on 0:off + property susFlag : UInt8 = 0u8 # key-sus option 1:on 0:off + property tll : UInt16 = 0u16 # total level + key scale level + property rks : UInt8 = 0u8 # key scale offset (rks) for eg speed + property egRateH : UInt8 = 0u8 # eg speed rate high 4bits + property egRateL : UInt8 = 0u8 # eg speed rate low 2bits + property egShift : UInt32 = 0u32 # shift for eg global counter, controls envelope speed + property egOut : UInt32 = 0u32 # eg output + + property updateRequests : UInt32 = 0u32 # flags to debounce update + + def initialize + end + + def reset(@number : UInt8) + @type = @number % 2 + @pgKeep = 0u8 + @waveTable = YM2413Emu2413.waveTableMap[0] + @pgPhase = 0u32 + @output[0] = 0 + @output[1] = 0 + @egState = RELEASE + @egShift = 0u32 + @rks = 0u8 + @tll = 0u16 + @keyFlag = 0u8 + @susFlag = 0u8 + @blk = 0u8 + @fnum = 0u16 + @volume = 0 + @pgOut = 0u32 + @egOut = EG_MUTE.to_u32! + @patch = Patch.new + end + + @[AlwaysInline] + def requestUpdate(flag : Int) + @updateRequests |= flag + end + + @[AlwaysInline] + def getParameterRate : UInt32 + return 0u32 if !Yuno.bitflag?(@type, 1) && @keyFlag == 0 + + case @egState + when ATTACK + @patch.ar + when DECAY + @patch.dr + when SUSTAIN + @patch.eg != 0 ? 0u32 : @patch.rr + when RELEASE + case + when @susFlag != 0 then 5u32 + when @patch.eg != 0 then @patch.rr + else 7u32 + end + when DAMP + DAMPER_RATE + else + 0u32 + end + end + + def commitUpdate : Nil + if Yuno.bitflag?(@updateRequests, UPDATE_WS) + @waveTable = YM2413Emu2413.waveTableMap[@patch.ws] + end + + if Yuno.bitflag?(@updateRequests, UPDATE_TLL) + if !Yuno.bitflag?(@type, 1) + @tll = YM2413Emu2413.tllTable[@blkFnum >> 5][@patch.tl][@patch.kl].to_u16! + else + @tll = YM2413Emu2413.tllTable[@blkFnum >> 5][@volume][@patch.kl].to_u16! + end + end + + if Yuno.bitflag?(@updateRequests, UPDATE_RKS) + @rks = YM2413Emu2413.rksTable[@blkFnum >> 8][@patch.kr].to_u8! + end + + if Yuno.bitflag?(@updateRequests, UPDATE_RKS | UPDATE_EG) + prate : UInt32 = getParameterRate + if prate == 0 + @egShift = 0u32 + @egRateH = 0u8 + @egRateL = 0u8 + return + end + + @egRateH = Math.min(15, prate + (@rks >> 2)).to_u8! + @egRateL = @rks & 3 + if @egState == ATTACK + @egShift = if 0 < @egRateH && @egRateH < 12 + 13u32 - @egRateH + else + 0u32 + end + else + @egShift = if @egRateH < 13 + 13u32 - @egRateH + else + 0u32 + end + end + end + + @updateRequests = 0u32 + end + + @[AlwaysInline] + def on : Nil + @keyFlag = 1u8 + @egState = DAMP + requestUpdate(UPDATE_EG) + end + + @[AlwaysInline] + def off : Nil + @keyFlag = 0u8 + if Yuno.bitflag?(@type, 1) + @egState = RELEASE + requestUpdate(UPDATE_EG) + end + end + + @[AlwaysInline] + def setVolume(@volume : Int32) : Nil + requestUpdate(UPDATE_TLL) + end + + @[AlwaysInline] + def calcPhase(pmPhase : Int32, reset : UInt8) : Nil + pm : Int8 = if @patch.pm != 0 + PM_TABLE[(@fnum >> 6) & 7][(pmPhase >> 10) & 7] + else + 0i8 + end + @pgPhase = 0 if reset != 0 + @pgPhase += (((@fnum.to_u32! & 0x1FF) * 2 + pm) * ML_TABLE[@patch.ml]) << @blk >> 2 + @pgPhase &= (DP_WIDTH - 1) + @pgOut = @pgPhase >> DP_BASE_BITS + end + + @[AlwaysInline] + def lookupAttackStep(counter : UInt32) : UInt8 + case @egRateH + when 12 + index : UInt32 = (counter & 0xC) >> 1 + 4u8 - EG_STEP_TABLES[@egRateL][index] + when 13 + index = (counter & 0xC) >> 1 + 3u8 - EG_STEP_TABLES[@egRateL][index] + when 14 + index = (counter & 0xC) >> 1 + 2u8 - EG_STEP_TABLES[@egRateL][index] + when 0, 15 + 0u8 + else + index = counter >> @egShift + EG_STEP_TABLES[@egRateL][index & 7] != 0 ? 4u8 : 0u8 + end + end + + @[AlwaysInline] + def lookupDecayStep(counter : UInt32) : UInt8 + case @egRateH + when 0 + 0u8 + when 13 + index : UInt32 = ((counter & 0xC) >> 1) | (counter & 1) + EG_STEP_TABLES[@egRateL][index] + when 14 + index = (counter & 0xC) >> 1 + EG_STEP_TABLES[@egRateL][index] + 1 + when 15 + 2u8 + else + index = counter >> @egShift + EG_STEP_TABLES[@egRateL][index & 7] + end + end + + @[AlwaysInline] + def startEnvelope : Nil + if Math.min(15, @patch.ar + (@rks >> 2)) == 15 + @egState = DECAY + @egOut = 0u32 + else + @egState = ATTACK + @egOut = EG_MUTE + end + requestUpdate(UPDATE_EG) + end + + @[AlwaysInline] + def calcEnvelope(buddy : Slot?, egCounter : UInt16, test : UInt8) : Nil + mask : UInt32 = (1u32 << @egShift) - 1 + s : UInt8 = 0u8 + + if @egState == ATTACK + if 0 < @egOut && 0 < @egRateH && (egCounter & mask & ~3) == 0 + s = lookupAttackStep(egCounter.to_u32!) + if 0 < s + @egOut = Math.max(0, @egOut.to_i32! - (@egOut >> s) - 1).to_u32! + end + end + else + if @egRateH > 0 && !Yuno.bitflag?(egCounter, mask) + @egOut = Math.min(EG_MUTE, @egOut + lookupDecayStep(egCounter.to_u32!)) + end + end + + case @egState + when DAMP + if @egOut >= EG_MUTE + startEnvelope + if Yuno.bitflag?(@type, 1) + if @pgKeep == 0 + @pgPhase = 0 + end + + buddy.try do |bdy| + bdy.pgPhase = 0 if bdy.pgKeep == 0 + end + end + end + + when ATTACK + if @egOut == 0 + @egState = DECAY + requestUpdate(UPDATE_EG) + end + + when DECAY + if (@egOut >> 3) == @patch.sl + @egState = SUSTAIN + requestUpdate(UPDATE_EG) + end + end + + @egOut = 0u32 if test != 0 + end + end + + private class RateConv + LW = 16 + SINC_RESO = 256 + SINC_AMP_BITS = 12 + SINC_TABLE_SIZE = (SINC_RESO * LW).tdiv(2) + SINC_TABLE_MAX = SINC_TABLE_SIZE - 1 + + property ch : Int32 = 0 + property timer : Float64 = 0.0 + property fRatio : Float64 = 0.0 + property sincTable : Array(Int16) + property buf : Array(Array(Int16)) + + def initialize(finp : Float64, fout : Float64, @ch : Int32) + @fRatio = finp / fout + @buf = Array(Array(Int16)).new(@ch) do |_| + Array(Int16).new(LW, 0i16) + end + + # Create @sincTable for positive 0 <= x < LW/2 + @sincTable = Array(Int16).new(SINC_TABLE_SIZE) do |i| + x : Float64 = i / SINC_RESO + if fout < finp + # Downsampling + ((1 << SINC_AMP_BITS) * RateConv.windowedSinc(x / @fRatio) / @fRatio).to_i16! + else + # Upsampling + ((1 << SINC_AMP_BITS) * RateConv.windowedSinc(x)).to_i16! + end + end + end + + protected def self.blackman(x : Float64) : Float64 + 0.42 - 0.5 * Math.cos(2 * Math::PI * x) + 0.08 * Math.cos(4 * Math::PI * x) + end + + protected def self.sinc(x : Float64) : Float64 + (x == 0.0 ? 1.0 : Math.sin(Math::PI * x) / (Math::PI * x)) + end + + protected def self.windowedSinc(x : Float64) : Float64 + blackman(0.5 + 0.5 * x / (LW / 2)) * sinc(x) + end + + def reset : Nil + @timer = 0 + @buf.each(&.fill(0i16)) + end + + # Out original data to this converter at finp + def put(ch : Int32, data : Int16) + buf = @buf[ch] + (LW - 1).times do |i| + buf[i] = buf[i + 1] + end + buf[LW - 1] = data + end + + # Get resampled data from this converter at fout. This function must be + # called fout / finp times per one #put call. + def get(ch : Int32) : Int16 + buf = @buf[ch] + sum : Int32 = 0 + @timer += @fRatio + dn : Float64 = @timer - @timer.floor + + x : Float64 = 0.0 + LW.times do |k| + x = (k.to_f64! - (LW / 2 - 1)) - dn + sum = sum + (buf[k].to_i32! * lookup(x)) + end + + (sum >> SINC_AMP_BITS).to_i16! + end + + @[AlwaysInline] + private def lookup(x : Float64) : Int16 + idx = (x * SINC_RESO).to_i16! + idx = -idx if idx < 0 + @sincTable[Math.min(SINC_TABLE_MAX, idx)] + end + end + + ### + ### Fields + ### + + @clock : UInt32 = 0u32 + @rate : UInt32 = 0u32 + @chipType : UInt8 = 0u8 + @addr : UInt32 = 0u32 + @inpStep : UInt32 = 0u32 + @outStep : UInt32 = 0u32 + @outTime : UInt32 = 0u32 + @reg : Array(UInt8) = Array(UInt8).new(0x40, 0u8) + @testFlag : UInt8 = 0u8 + @slotKeyStatus : UInt32 = 0u32 + @rhythmMode : UInt8 = 0u8 + @egCounter : UInt32 = 0u32 + @pmPhase : UInt32 = 0u32 + @amPhase : Int32 = 0u32 + @lfoAm : UInt8 = 0u8 + @noise : UInt32 = 0u32 + @shortNoise : UInt8 = 0u8 + @patchNumber : Array(Int32) = Array(Int32).new(9, 0) + @slot : Array(Slot) + @patch : Array(Patch) + @pan : Array(UInt8) = Array(UInt8).new(16, 0u8) + @panFine : Array(Array(Float32)) + @mask : UInt32 = 0u32 + + # Channel output. + # + # 0..8: tone + # 9: bd + # 10: hh + # 11: sd + # 12: tom + # 13: cym + @chOut : Array(Int16) = Array(Int16).new(14, 0i16) + @mixOut : Array(Int16) = [0, 0] of Int16 + @buffers : Array(Int32) = [0, 0] + @conv : RateConv? + + ### + ### Constructor + ### + + def initialize(@clock : UInt32, @rate : UInt32) + YM2413Emu2413.initTables + + @slot = Array(Slot).new(18) { |_| Slot.new } + @patch = Array(Patch).new(19 * 2) { |_| Patch.new } + @panFine = Array(Array(Float32)).new(16) do |_| + [0.0f32, 0.0f32] + end + + reset + setChipType(0) + resetPatch(0) + end + + def resetRateConversionParams : Nil + fout : Float64 = @rate.to_f64! + finp : Float64 = @clock / 72 + + @outTime = 0 + @outStep = finp.to_u32! << 8 + @inpStep = fout.to_u32! << 8 + + @conv = nil + if finp.floor != fout && (finp + 0.5).floor != fout + @conv = RateConv.new(finp, fout, 2) + end + @conv.try(&.reset) + end + + def reset : Nil + @addr = 0u32 + @pmPhase = 0u32 + @amPhase = 0u32 + @noise = 1u32 + @rhythmMode = 0u8 + @slotKeyStatus = 0u32 + @egCounter = 0u32 + + resetRateConversionParams + + @slot.each_with_index { |slt, i| slt.reset(i.to_u8) } + 9.times { |i| setPatch(i, 0) } + 0x40_u32.times { |i| writeReg(i, 0u8) } + + 15.times do |i| + @pan[i] = 3 + centerPanning(@panFine[i]) + end + + @chOut.fill(0i16) + end + + def setChipType(ct : Int) : Nil + @chipType = ct.to_u8 + end + + def resetPatch(typ : Int) : Nil + (19 * 2).times do |i| + @patch[i].copy(@@defaultPatch[typ % OPLL_TONE_NUM][i]) + end + end + + def patch=(dump : Array(UInt8)) : Nil + ptr = dump.to_unsafe + + 19.times do |i| + ptch = Patch.fromDump(ptr + (i * 8)) + @patch[i * 2].copy(ptch[0]) + @patch[i * 2 + 1].copy(ptch[1]) + end + end + + def rate=(@rate : UInt32) : Nil + resetRateConversionParams + end + + def forceRefresh : Nil + 9.times do |i| + setPatch(i, @patchNumber[i]) + end + + @slot.each(&.requestUpdate(UPDATE_ALL)) + end + + def writeReg(reg : UInt32, data : UInt8) : Nil + return if reg >= 0x40 + + # Mirror registers + if (0x19 <= reg && reg <= 0x1F) || + (0x29 <= reg && reg <= 0x2F) || + (0x39 <= reg && reg <= 0x3F) + reg -= 9 + end + + @reg[reg] = data + + case reg + when 0x00 + @patch[0].am = ((data >> 7) & 1).to_u32! + @patch[0].pm = ((data >> 6) & 1).to_u32! + @patch[0].eg = ((data >> 5) & 1).to_u32! + @patch[0].kr = ((data >> 4) & 1).to_u32! + @patch[0].ml = (data & 15).to_u32! + 9.times do |i| + if @patchNumber[i] == 0 + mod(i).requestUpdate(UPDATE_RKS|UPDATE_EG) + end + end + + when 0x01 + @patch[1].am = ((data >> 7) & 1).to_u32! + @patch[1].pm = ((data >> 6) & 1).to_u32! + @patch[1].eg = ((data >> 5) & 1).to_u32! + @patch[1].kr = ((data >> 4) & 1).to_u32! + @patch[1].ml = (data & 15).to_u32! + 9.times do |i| + if @patchNumber[i] == 0 + car(i).requestUpdate(UPDATE_RKS|UPDATE_EG) + end + end + + when 0x02 + @patch[0].kl = ((data >> 6) & 3).to_u32! + @patch[0].tl = (data & 63).to_u32! + 9.times do |i| + if @patchNumber[i] == 0 + mod(i).requestUpdate(UPDATE_TLL) + end + end + + when 0x03 + @patch[1].kl = ((data >> 6) & 3).to_u32! + @patch[1].ws = ((data >> 4) & 1).to_u32! + @patch[0].ws = ((data >> 3) & 1).to_u32! + @patch[0].fb = (data & 7).to_u32! + 9.times do |i| + if @patchNumber[i] == 0 + mod(i).requestUpdate(UPDATE_WS) + car(i).requestUpdate(UPDATE_WS|UPDATE_TLL) + end + end + + when 0x04 + @patch[0].ar = ((data >> 4) & 15).to_u32! + @patch[0].dr = (data & 15).to_u32! + 9.times do |i| + if @patchNumber[i] == 0 + mod(i).requestUpdate(UPDATE_EG) + end + end + + when 0x05 + @patch[1].ar = ((data >> 4) & 15).to_u32! + @patch[1].dr = (data & 15).to_u32! + 9.times do |i| + if @patchNumber[i] == 0 + car(i).requestUpdate(UPDATE_EG) + end + end + + when 0x06 + @patch[0].sl = ((data >> 4) & 15).to_u32! + @patch[0].rr = (data & 15).to_u32! + 9.times do |i| + if @patchNumber[i] == 0 + mod(i).requestUpdate(UPDATE_EG) + end + end + + when 0x07 + @patch[1].sl = ((data >> 4) & 15).to_u32! + @patch[1].rr = (data & 15).to_u32! + 9.times do |i| + if @patchNumber[i] == 0 + car(i).requestUpdate(UPDATE_EG) + end + end + + when 0x0E + unless @chipType == 1 + updateRhythmMode + updateKeyStatus + end + + when 0x0F + @testFlag = data + + when 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18 + ch : UInt32 = reg - 0x10 + setFnumber(ch, data.to_u32! + ((@reg[0x20 + ch].to_u32! & 1) << 8)) + + when 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28 + ch = reg - 0x20 + setFnumber(ch, ((data & 1).to_u32! << 8) + @reg[0x10 + ch]) + setBlock(ch, (data >> 1) & 7) + setSusFlag(ch, (data >> 5) & 1) + updateKeyStatus + + when 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38 + if Yuno.bitflag?(@reg[0x0E], 32) && reg >= 0x36 + case reg + when 0x37 + mod(7).setVolume(((data.to_i32! >> 4) & 15) << 2) + when 0x38 + mod(8).setVolume(((data.to_i32! >> 4) & 15) << 2) + end + else + setPatch((reg - 0x30).to_i32!, ((data >> 4) & 15).to_i32!) + end + setVolume((reg - 0x30).to_i32!, (data & 15).to_i32! << 2) + end + end + + def writeIO(addr : UInt32, val : UInt8) : Nil + if Yuno.bitflag?(addr, 1) + writeReg(@addr, val) + else + @addr = val.to_u32! + end + end + + def setPan(ch : UInt32, pan : UInt8) : Nil + @pan[ch & 15] = pan + end + + def setPanEx(ch : UInt32, pan : Int16) : Nil + calcPanning(@panFine[ch & 15], pan) # Maxim/Valley Bell + end + + def setPanFine(ch : UInt32, pan : Array(Float32)) : Nil + @panFine[ch & 15][0] = pan[0] + @panFine[ch & 15][1] = pan[1] + end + + def mask=(mask : UInt32) : UInt32 + ret = @mask + @mask = mask + ret + end + + def toggleMask(mask : UInt32) : UInt32 + ret = @mask + @mask ^= mask + ret + end + + def update(outputs : OutputBuffers, samples : UInt32) : Nil + outL = outputs[0] + outR = outputs[1] + + samples.times do |i| + calcStereo + outL[i] = @buffers.get!(0) + outR[i] = @buffers.get!(1) + end + end + + ### + ### Private methods + ### + + private def calcPanning(channels : Array(Float32), position : Int) : Nil + position = position.clamp(-(PANNING_RANGE.tdiv(2)), PANNING_RANGE.tdiv(2)) + position += PANNING_RANGE.tdiv(2) # make -256..0..256 -> 0..256..512 + + # Equal power law: equation is + # right = sin( position / range * pi / 2) * sqrt( 2 ) + # left is equivalent to right with position = range - position + # position is in the range 0 .. RANGE + # RANGE / 2 = centre, result = 1.0f + channels[1] = (Math.sin(position / PANNING_RANGE * Math::PI / 2) * SQRT2).to_f32! + position = PANNING_RANGE - position + channels[0] = (Math.sin(position / PANNING_RANGE * Math::PI / 2) * SQRT2).to_f32! + end + + private def centerPanning(channels : Array(Float32)) : Nil + channels.fill(1.0f32) + end + + @[AlwaysInline] + private def mod(x) + @slot[x << 1] + end + + @[AlwaysInline] + private def car(x) + @slot[x << 1 | 1] + end + + @[AlwaysInline] + private def bit(s, b) : UInt8 + ((s >> b) & 1).to_u8! + end + + @[AlwaysInline] + private def setPatch(ch : Int32, num : Int32) : Nil + @patchNumber[ch] = num + mod(ch).patch = @patch[num * 2] + car(ch).patch = @patch[num * 2 + 1] + mod(ch).requestUpdate(UPDATE_ALL) + car(ch).requestUpdate(UPDATE_ALL) + end + + @[AlwaysInline] + private def updateKeyStatus : Nil + r14 : UInt8 = @reg[0x0E] + rhythmMode : UInt8 = bit(r14, 5) + newSlotKeyStatus : UInt32 = 0u32 + + 9.times do |ch| + if Yuno.bitflag?(@reg[0x20 + ch], 0x10) + newSlotKeyStatus |= (3 << (ch * 2)) + end + end + + if rhythmMode != 0 + newSlotKeyStatus |= (3 << SLOT_BD1) if Yuno.bitflag?(r14, 0x10) + newSlotKeyStatus |= (1 << SLOT_HH ) if Yuno.bitflag?(r14, 0x01) + newSlotKeyStatus |= (1 << SLOT_SD ) if Yuno.bitflag?(r14, 0x08) + newSlotKeyStatus |= (1 << SLOT_TOM) if Yuno.bitflag?(r14, 0x04) + newSlotKeyStatus |= (1 << SLOT_CYM) if Yuno.bitflag?(r14, 0x02) + end + + updatedStatus : UInt32 = @slotKeyStatus ^ newSlotKeyStatus + + if updatedStatus != 0 + 18.times do |i| + if bit(updatedStatus, i) != 0 + if bit(newSlotKeyStatus, i) != 0 + @slot[i].on + else + @slot[i].off + end + end + end + end + + @slotKeyStatus = newSlotKeyStatus + end + + @[AlwaysInline] + private def setSusFlag(ch : Int, flag : Int) : Nil + car(ch).susFlag = flag.to_u8! + car(ch).requestUpdate(UPDATE_EG) + if Yuno.bitflag?(mod(ch).type, 1) + mod(ch).susFlag = flag.to_u8! + mod(ch).requestUpdate(UPDATE_EG) + end + end + + # Set volume ( volume : 6bit, register value << 2 ) + @[AlwaysInline] + private def setVolume(ch : Int, volume : Int32) : Nil + car(ch).volume = volume + car(ch).requestUpdate(UPDATE_TLL) + end + + # Set f-Nnmber ( fnum : 9bit ) + @[AlwaysInline] + private def setFnumber(ch : Int, fnum : Int) + carrier = car(ch) + modulator = mod(ch) + carrier.fnum = fnum.to_u16! + carrier.blkFnum = (carrier.blkFnum & 0xE00) | (fnum & 0x1FF) + modulator.fnum = fnum.to_u16! + modulator.blkFnum = (modulator.blkFnum & 0xE00) | (fnum & 0x1FF) + carrier.requestUpdate(UPDATE_EG | UPDATE_RKS | UPDATE_TLL) + modulator.requestUpdate(UPDATE_EG | UPDATE_RKS | UPDATE_TLL) + end + + # Set block data (blk : 3bit ) + @[AlwaysInline] + private def setBlock(ch : Int, blk : Int) + carrier = car(ch) + modulator = mod(ch) + carrier.blk = blk.to_u8! + carrier.blkFnum = (((blk.to_u32! & 7) << 9) | (carrier.blkFnum & 0x1FF)).to_u16! + modulator.blk = blk.to_u8! + modulator.blkFnum = (((blk.to_u32! & 7) << 9) | (modulator.blkFnum & 0x1FF)).to_u16! + carrier.requestUpdate(UPDATE_EG | UPDATE_RKS | UPDATE_TLL) + modulator.requestUpdate(UPDATE_EG | UPDATE_RKS | UPDATE_TLL) + end + + @[AlwaysInline] + private def updateRhythmMode : Nil + newRhythmMode : UInt8 = (@reg[0x0E] >> 5) & 1 + + if @rhythmMode != newRhythmMode + if newRhythmMode != 0 + @slot[SLOT_HH].type = 3u8 + @slot[SLOT_HH].pgKeep = 1u8 + @slot[SLOT_SD].type = 3u8 + @slot[SLOT_TOM].type = 3u8 + @slot[SLOT_CYM].type = 3u8 + @slot[SLOT_CYM].pgKeep = 1u8 + setPatch(6, 16) + setPatch(7, 17) + setPatch(8, 18) + @slot[SLOT_HH].setVolume(((@reg[0x37] >> 4) & 15).to_i32! << 2) + @slot[SLOT_TOM].setVolume(((@reg[0x38] >> 4) & 15).to_i32! << 2) + else + @slot[SLOT_HH].type = 0u8 + @slot[SLOT_HH].pgKeep = 0u8 + @slot[SLOT_SD].type = 1u8 + @slot[SLOT_TOM].type = 0u8 + @slot[SLOT_CYM].type = 1u8 + @slot[SLOT_CYM].pgKeep = 0u8 + setPatch(6, (@reg[0x36] >> 4).to_i32!) + setPatch(7, (@reg[0x37] >> 4).to_i32!) + setPatch(8, (@reg[0x38] >> 4).to_i32!) + end + end + + @rhythmMode = newRhythmMode + end + + private def updateAmpm : Nil + if Yuno.bitflag?(@testFlag, 2) + @pmPhase = 0u32 + @amPhase = 0 + else + @pmPhase += (Yuno.bitflag?(@testFlag, 8) != 0 ? 1024u32 : 1u32) + @amPhase += (Yuno.bitflag?(@testFlag, 8) != 0 ? 64 : 1) + end + @lfoAm = AM_TABLE[(@amPhase >> 6) % AM_TABLE.size] + end + + private def updateNoise(cycle : Int) : Nil + cycle.times do |i| + if Yuno.bitflag?(@noise, 1) + @noise ^= 0x800200 + end + @noise >>= 1 + end + end + + private def updateShortNoise : Nil + pgHh : UInt32 = @slot[SLOT_HH].pgOut + pgCym : UInt32 = @slot[SLOT_CYM].pgOut + + hBit2 : UInt8 = bit(pgHh, PG_BITS - 8) + hBit7 : UInt8 = bit(pgHh, PG_BITS - 3) + hBit3 : UInt8 = bit(pgHh, PG_BITS - 7) + + cBit3 : UInt8 = bit(pgCym, PG_BITS - 7) + cBit5 : UInt8 = bit(pgCym, PG_BITS - 5) + + @shortNoise = (hBit2 ^ hBit7) | (hBit3 ^ cBit5) | (cBit3 ^ cBit5) + end + + private def updateSlots : Nil + @egCounter += 1 + + buddy : Slot? = nil + 18.times do |i| + slt = @slot[i] + buddy = nil + if slt.type == 0 + buddy = @slot[i + 1] + end + + if slt.type == 1 + buddy = @slot[i - 1] + end + + if slt.updateRequests != 0 + slt.commitUpdate + end + + slt.calcEnvelope(buddy, @egCounter.to_u16!, @testFlag & 1) + slt.calcPhase(@pmPhase.to_i32!, @testFlag & 4) + end + end + + # Output: -4095...4095 + @[AlwaysInline] + private def lookupExpTable(i : UInt16) : Int16 + # From andete's expression + t : Int16 = (EXP_TABLE[(i & 0xFF) ^ 0xFF] + 1024).to_i16! + res : Int16 = t >> ((i & 0x7F00) >> 8) + (Yuno.bitflag?(i, 0x8000) ? ~res : res) << 1 + end + + @[AlwaysInline] + def toLinear(h : UInt16, slt : Slot, am : Int16) : Int16 + if slt.egOut >= EG_MAX + 0i16 + else + att = Math.min(EG_MAX, (slt.egOut + slt.tll + am)) << 4 + lookupExpTable(h + att) + end + end + + @[AlwaysInline] + private def calcSlotCar(ch : Int, fm : Int16) : Int16 + slt : Slot = car(ch) + am : UInt8 = slt.patch.am != 0 ? @lfoAm : 0u8 + + slt.output[1] = slt.output[0] + slt.output[0] = toLinear(slt.waveTable[(slt.pgOut.to_i32! + 2 * (fm >> 1)) & (PG_WIDTH - 1)], slt, am.to_i16!) + + slt.output[0].to_i16! + end + + @[AlwaysInline] + private def calcSlotMod(ch : Int) : Int16 + slt : Slot = mod(ch) + fm : Int16 = if slt.patch.fb > 0 + ((slt.output[1].to_i32! + slt.output[0]) >> (9 - slt.patch.fb)).to_i16! + else + 0i16 + end + am : UInt8 = slt.patch.am != 0 ? @lfoAm : 0u8 + + slt.output[1] = slt.output[0] + slt.output[0] = toLinear(slt.waveTable[(slt.pgOut.to_i32! + fm) & (PG_WIDTH - 1)], slt, am.to_i16!) + + slt.output[0].to_i16! + end + + @[AlwaysInline] + private def calcSlotTom : Int16 + slt = mod(8) + toLinear(slt.waveTable[slt.pgOut], slt, 0) + end + + @[AlwaysInline] + private def calcPd(phase) : UInt32 + #if PG_BITS < 10 + # phase >> (10 - PG_BITS) + #else + (phase << (PG_BITS - 10)).to_u32! + #end + end + + @[AlwaysInline] + private def calcSlotSnare : Int16 + slt : Slot = car(7) + phase : UInt32 = if bit(slt.pgOut, PG_BITS - 2) != 0 + Yuno.bitflag?(@noise, 1) ? calcPd(0x300) : calcPd(0x200) + else + Yuno.bitflag?(@noise, 1) ? calcPd(0x0) : calcPd(0x100) + end + toLinear(slt.waveTable[phase], slt, 0) + end + + @[AlwaysInline] + private def calcSlotCym : Int16 + slt : Slot = car(8) + phase : UInt32 = @shortNoise != 0 ? calcPd(0x300) : calcPd(0x100) + toLinear(slt.waveTable[phase], slt, 0) + end + + @[AlwaysInline] + private def calcSlotHat : Int16 + slt = mod(7) + phase = if @shortNoise != 0 + Yuno.bitflag?(@noise, 1) ? calcPd(0x2D0) : calcPd(0x234) + else + Yuno.bitflag?(@noise, 1) ? calcPd(0x34) : calcPd(0xD0) + end + toLinear(slt.waveTable[phase], slt, 0) + end + + @[AlwaysInline] + private def mo(x) + -x >> 1 + end + + private def updateOutput : Nil + updateAmpm + updateShortNoise + updateSlots + + out = @chOut + + # CH1-6 + 6.times do |i| + unless Yuno.bitflag?(@mask, maskCh(i)) + out[i] = mo(calcSlotCar(i, calcSlotMod(i))) + end + end + + # CH7 + if @rhythmMode == 0 + unless Yuno.bitflag?(@mask, maskCh(6)) + out[6] = mo(calcSlotCar(6, calcSlotMod(6))) + end + else + unless Yuno.bitflag?(@mask, MASK_BD) + out[9] = calcSlotCar(6, calcSlotMod(6)) + end + end + updateNoise(14) + + # CH8 + if @rhythmMode == 0 + unless Yuno.bitflag?(@mask, maskCh(7)) + out[7] = mo(calcSlotCar(7, calcSlotMod(7))) + end + else + unless Yuno.bitflag?(@mask, MASK_HH) + out[10] = calcSlotHat + end + unless Yuno.bitflag?(@mask, MASK_SD) + out[11] = calcSlotSnare + end + end + updateNoise(2) + + # CH9 + if @rhythmMode == 0 + unless Yuno.bitflag?(@mask, maskCh(8)) + out[8] = mo(calcSlotCar(8, calcSlotMod(8))) + end + else + unless Yuno.bitflag?(@mask, MASK_TOM) + out[12] = calcSlotTom + end + unless Yuno.bitflag?(@mask, MASK_CYM) + out[13] = calcSlotCym + end + end + updateNoise(2) + end + + @[AlwaysInline] + private def mixOutputStereo : Nil + out = @mixOut + out.fill(0) + 14.times do |i| + # Maxim/Valley Bell: added stereo control (multiply each side by a float in opll->pan[ch][side]) */ + if Yuno.bitflag?(@pan[i], 2) + out[0] = out[0] + (@chOut[i] * @panFine[i][0]).to_i32! + end + + if Yuno.bitflag?(@pan[i], 1) + out[1] = out[1] + (@chOut[i] * @panFine[i][1]).to_i32! + end + end + + @conv.try do |cnv| + cnv.put(0, @buffers[0].to_i16!) + cnv.put(1, @buffers[1].to_i16!) + end + end + + private def calcStereo : Nil + while @outStep > @outTime + @outTime = @outTime + @inpStep + updateOutput + mixOutputStereo + end + + @outTime = @outTime - @outStep + if cnv = @conv + @buffers[0] = cnv.get(0) + @buffers[1] = cnv.get(1) + else + @buffers[0] = @mixOut[0].to_i32! + @buffers[1] = @mixOut[1].to_i32! + #@buffers.to_unsafe.copy_from(@mixOut.to_unsafe, 2) + end + end + + ### + ### Class Methods + ### + + protected def self.makeSinTable : Nil + # Remi: Adjust @@fullsinTable so that it's PG_WIDTH large. + (PG_WIDTH - @@fullsinTable.size).times do |_| + @@fullsinTable << 0u16 + end + + PG_WIDTH.tdiv(4).times do |x| + @@fullsinTable[PG_WIDTH.tdiv(4) + x] = @@fullsinTable[PG_WIDTH.tdiv(4) - x - 1] + end + + PG_WIDTH.tdiv(2).times do |x| + @@fullsinTable[PG_WIDTH.tdiv(2) + x] = 0x8000_u16 | @@fullsinTable[x] + end + + PG_WIDTH.tdiv(2).times do |x| + @@halfsinTable[x] = @@fullsinTable[x] + end + + (PG_WIDTH.tdiv(2)...PG_WIDTH).each do |x| + @@halfsinTable[x] = 0xFFF_u16 + end + end + + protected def self.makeTllTable : Nil + tmp : Int32 = 0 + + # Remi: Create properly sized table + @@tllTable = Array(Array(Array(UInt32))).new(8 * 16) do |_| + Array(Array(UInt32)).new(1 << TL_BITS) do |_| + Array(UInt32).new(4, 0u32) + end + end + + 16.times do |fnum| + 8.times do |block| + 64u32.times do |tl| + 4.times do |kl| + if kl == 0 + @@tllTable[(block << 4) | fnum][tl][kl] = tl2eg(tl) + else + tmp = (KL_TABLE[fnum].to_f64! - (3.0 * 2) * (7 - block)).to_i32! + if tmp <= 0 + @@tllTable[(block << 4) | fnum][tl][kl] = tl2eg(tl) + else + @@tllTable[(block << 4) | fnum][tl][kl] = ((tmp >> (3 - kl)) / EG_STEP).to_u32! + tl2eg(tl) + end + end + end + end + end + end + end + + protected def self.makeRksTable : Nil + # Remi: Create properly sized table + @@rksTable = Array(Array(Int32)).new(8 * 2) { |_| [0, 0] } + + 2.times do |fnum8| + 8.times do |block| + @@rksTable[(block << 1) | fnum8][1] = (block << 1) + fnum8 + @@rksTable[(block << 1) | fnum8][0] = block >> 1 + end + end + end + + protected def self.makeDefaultPatch : Nil + # Remi: Create properly sized array + @@defaultPatch = Array(Array(Patch)).new(OPLL_TONE_NUM) do |_| + Array(Patch).new((16 + 3) * 2) { |_| Patch.new } + end + + OPLL_TONE_NUM.times do |i| + 19.times do |j| + ptch = Patch.getDefaultPatch(i, j) + @@defaultPatch[i][j * 2] = ptch[0] + @@defaultPatch[i][j * 2 + 1] = ptch[1] + end + end + end + + protected def self.initTables : Nil + return if @@tablesMade + makeTllTable + makeRksTable + makeSinTable + makeDefaultPatch + @@tablesMade = true + end + end + end +end Index: src/yunosynth/common.cr ================================================================== --- src/yunosynth/common.cr +++ src/yunosynth/common.cr @@ -379,10 +379,12 @@ when .saa1099? then Yuno::Chips::Saa1099.new(curCount, absChipCount, vgm, sampleRate, samplingMode, playerSampleRate) when .pwm? then Yuno::Chips::Pwm.new(curCount, absChipCount, vgm, sampleRate, samplingMode, playerSampleRate) when .wonderswan? then Yuno::Chips::Wonderswan.new(curCount, absChipCount, vgm, sampleRate, samplingMode, playerSampleRate) + when .ym2413? then Yuno::Chips::YM2413.new(curCount, absChipCount, vgm, sampleRate, samplingMode, + playerSampleRate) when .unknown? then raise "Attempted to start unknown chip" else nil end end @@ -523,10 +525,11 @@ ChipType::Wonderswan, ChipType::X1_010, ChipType::Y8950, ChipType::Ym2151, ChipType::Ym2203, + ChipType::Ym2413, ChipType::Ym2608, ChipType::Ym2610, ChipType::Ym2612, ChipType::Ym3526, ChipType::Ym3812, @@ -540,11 +543,10 @@ def self.unsupportedChips : Array(ChipType) [ ChipType::Es5506, ChipType::Pokey, ChipType::Scsp, - ChipType::Ym2413, ChipType::Ymf271, ChipType::Ymf278b, ChipType::Rf5c68 ] end Index: src/yunosynth/dac-controller.cr ================================================================== --- src/yunosynth/dac-controller.cr +++ src/yunosynth/dac-controller.cr @@ -481,11 +481,11 @@ ## Other chips (for completeness) ## # 8-bit register, 8-bit data when .ym2151?, .ym2203?, .ay8910?, .ymz280b?, .multi_pcm?, .dmg?, .okim6258?, .k053260?, .upd7759?, .y8950?, - .ym3526?, .nes_apu? + .ym3526?, .nes_apu?, .ym2413? command = (chip.destCommand & 0x00FF).to_u8! data = chipData[0] chipInstance = @chipTable[chip.destChipType][chip.destChipIndex] chipInstance.writeDac(0, command, data) Index: src/yunosynth/vgmplayer.cr ================================================================== --- src/yunosynth/vgmplayer.cr +++ src/yunosynth/vgmplayer.cr @@ -372,10 +372,11 @@ when 0xC8 then @ip += x1_010Write when 0xBD then @ip += saa1099Write when 0xB2 then @ip += pwmWrite when 0xBC then @ip += wonderswanWrite when 0xC6 then @ip += wonderswanRAMWrite + when 0x51 then @ip += ym2413Write(curChip) ### ### DAC Commands ### Index: src/yunosynth/vgmplayerinst.cr ================================================================== --- src/yunosynth/vgmplayerinst.cr +++ src/yunosynth/vgmplayerinst.cr @@ -400,10 +400,19 @@ # chip = @chipTable[ChipType::Ymf271][curChip].as(Yuno::Chips::YMF271) # port = (@vgm.data[@ip + 1] & 0x7F) # chip.write(curChip, 1, port, @vgm.data[@ip + 2], @vgm.data[@ip + 3]) # 4 # end + + # Writes to the YM2413 core's registers + @[AlwaysInline] + private def ym2413Write(curChip : UInt8) : Int32 + chip = @chipTable[ChipType::Ym2413][curChip].as(Yuno::Chips::YM2413) + chip.write(curChip, 0x00, @vgm.data[@ip + 1]) + chip.write(curChip, 0x01, @vgm.data[@ip + 2]) + 3 + end # Writes to the PWM core's registers @[AlwaysInline] private def pwmWrite : Int32 curChip : UInt8 = ((@vgm.data[@ip + 1] & 0x80) >> 7).to_u8!