diff --git a/CMakeLists.txt b/CMakeLists.txt index b47d2a74..ff7db42e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -368,6 +368,24 @@ src/engine/platform/sound/c64/wave8580_PST.cc src/engine/platform/sound/c64/wave8580_P_T.cc src/engine/platform/sound/c64/wave8580__ST.cc +src/engine/platform/sound/c64_fp/Dac.cpp +src/engine/platform/sound/c64_fp/EnvelopeGenerator.cpp +src/engine/platform/sound/c64_fp/ExternalFilter.cpp +src/engine/platform/sound/c64_fp/Filter6581.cpp +src/engine/platform/sound/c64_fp/Filter8580.cpp +src/engine/platform/sound/c64_fp/Filter.cpp +src/engine/platform/sound/c64_fp/FilterModelConfig6581.cpp +src/engine/platform/sound/c64_fp/FilterModelConfig8580.cpp +src/engine/platform/sound/c64_fp/FilterModelConfig.cpp +src/engine/platform/sound/c64_fp/Integrator6581.cpp +src/engine/platform/sound/c64_fp/Integrator8580.cpp +src/engine/platform/sound/c64_fp/OpAmp.cpp +src/engine/platform/sound/c64_fp/SID.cpp +src/engine/platform/sound/c64_fp/Spline.cpp +src/engine/platform/sound/c64_fp/WaveformCalculator.cpp +src/engine/platform/sound/c64_fp/WaveformGenerator.cpp +src/engine/platform/sound/c64_fp/resample/SincResampler.cpp + src/engine/platform/sound/tia/TIASnd.cpp src/engine/platform/sound/ymfm/ymfm_adpcm.cpp diff --git a/src/engine/platform/sound/c64_fp/AUTHORS b/src/engine/platform/sound/c64_fp/AUTHORS new file mode 100644 index 00000000..b04ee0f0 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/AUTHORS @@ -0,0 +1,6 @@ +Authors of reSIDfp. + +Dag Lem: Designed and programmed complete emulation engine. +Antti S. Lankila: Distortion simulation and calculation of combined waveforms +Ken Händel: source code conversion to Java +Leandro Nini: port to c++, merge with reSID 1.0 diff --git a/src/engine/platform/sound/c64_fp/COPYING b/src/engine/platform/sound/c64_fp/COPYING new file mode 100644 index 00000000..d159169d --- /dev/null +++ b/src/engine/platform/sound/c64_fp/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/src/engine/platform/sound/c64_fp/Dac.cpp b/src/engine/platform/sound/c64_fp/Dac.cpp new file mode 100644 index 00000000..0665da81 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Dac.cpp @@ -0,0 +1,123 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2016 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "Dac.h" + +namespace reSIDfp +{ + +Dac::Dac(unsigned int bits) : + dac(new double[bits]), + dacLength(bits) +{} + +Dac::~Dac() +{ + delete [] dac; +} + +double Dac::getOutput(unsigned int input) const +{ + double dacValue = 0.; + + for (unsigned int i = 0; i < dacLength; i++) + { + if ((input & (1 << i)) != 0) + { + dacValue += dac[i]; + } + } + + return dacValue; +} + +void Dac::kinkedDac(ChipModel chipModel) +{ + const double R_INFINITY = 1e6; + + // Non-linearity parameter, 8580 DACs are perfectly linear + const double _2R_div_R = chipModel == MOS6581 ? 2.20 : 2.00; + + // 6581 DACs are not terminated by a 2R resistor + const bool term = chipModel == MOS8580; + + // Calculate voltage contribution by each individual bit in the R-2R ladder. + for (unsigned int set_bit = 0; set_bit < dacLength; set_bit++) + { + double Vn = 1.; // Normalized bit voltage. + double R = 1.; // Normalized R + const double _2R = _2R_div_R * R; // 2R + double Rn = term ? // Rn = 2R for correct termination, + _2R : R_INFINITY; // INFINITY for missing termination. + + unsigned int bit; + + // Calculate DAC "tail" resistance by repeated parallel substitution. + for (bit = 0; bit < set_bit; bit++) + { + Rn = (Rn == R_INFINITY) ? + R + _2R : + R + (_2R * Rn) / (_2R + Rn); // R + 2R || Rn + } + + // Source transformation for bit voltage. + if (Rn == R_INFINITY) + { + Rn = _2R; + } + else + { + Rn = (_2R * Rn) / (_2R + Rn); // 2R || Rn + Vn = Vn * Rn / _2R; + } + + // Calculate DAC output voltage by repeated source transformation from + // the "tail". + + for (++bit; bit < dacLength; bit++) + { + Rn += R; + const double I = Vn / Rn; + Rn = (_2R * Rn) / (_2R + Rn); // 2R || Rn + Vn = Rn * I; + } + + dac[set_bit] = Vn; + } + + // Normalize to integerish behavior + double Vsum = 0.; + + for (unsigned int i = 0; i < dacLength; i++) + { + Vsum += dac[i]; + } + + Vsum /= 1 << dacLength; + + for (unsigned int i = 0; i < dacLength; i++) + { + dac[i] /= Vsum; + } +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/Dac.h b/src/engine/platform/sound/c64_fp/Dac.h new file mode 100644 index 00000000..35bc0b2c --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Dac.h @@ -0,0 +1,111 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2016 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef DAC_H +#define DAC_H + +#include "siddefs-fp.h" + +namespace reSIDfp +{ + +/** + * Estimate DAC nonlinearity. + * The SID DACs are built up as R-2R ladder as follows: + * + * n n-1 2 1 0 VGND + * | | | | | | Termination + * 2R 2R 2R 2R 2R 2R only for + * | | | | | | MOS 8580 + * Vo -o-R-o-R-...-o-R-o-R-- --+ + * + * + * All MOS 6581 DACs are missing a termination resistor at bit 0. This causes + * pronounced errors for the lower 4 - 5 bits (e.g. the output for bit 0 is + * actually equal to the output for bit 1), resulting in DAC discontinuities + * for the lower bits. + * In addition to this, the 6581 DACs exhibit further severe discontinuities + * for higher bits, which may be explained by a less than perfect match between + * the R and 2R resistors, or by output impedance in the NMOS transistors + * providing the bit voltages. A good approximation of the actual DAC output is + * achieved for 2R/R ~ 2.20. + * + * The MOS 8580 DACs, on the other hand, do not exhibit any discontinuities. + * These DACs include the correct termination resistor, and also seem to have + * very accurately matched R and 2R resistors (2R/R = 2.00). + * + * On the 6581 the output of the waveform and envelope DACs go through + * a voltage follower built with two NMOS: + * + * Vdd + * + * | + * |-+ + * Vin -------| T1 (enhancement-mode) + * |-+ + * | + * o-------- Vout + * | + * |-+ + * +---| T2 (depletion-mode) + * | |-+ + * | | + * + * GND GND + */ +class Dac +{ +private: + /// analog values + double * const dac; + + /// the dac array length + const unsigned int dacLength; + +public: + /** + * Initialize DAC model. + * + * @param bits the number of input bits + */ + Dac(unsigned int bits); + ~Dac(); + + /** + * Build DAC model for specific chip. + * + * @param chipModel 6581 or 8580 + */ + void kinkedDac(ChipModel chipModel); + + /** + * Get the Vo output for a given combination of input bits. + * + * @param input the digital input + * @return the analog output value + */ + double getOutput(unsigned int input) const; +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/EnvelopeGenerator.cpp b/src/engine/platform/sound/c64_fp/EnvelopeGenerator.cpp new file mode 100644 index 00000000..af636ac7 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/EnvelopeGenerator.cpp @@ -0,0 +1,155 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2020 Leandro Nini + * Copyright 2018 VICE Project + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define ENVELOPEGENERATOR_CPP + +#include "EnvelopeGenerator.h" + +namespace reSIDfp +{ + +/** + * Lookup table to convert from attack, decay, or release value to rate + * counter period. + * + * The rate counter is a 15 bit register which is left shifted each cycle. + * When the counter reaches a specific comparison value, + * the envelope counter is incremented (attack) or decremented + * (decay/release) and the rate counter is resetted. + * + * see [kevtris.org](http://blog.kevtris.org/?p=13) + */ +const unsigned int EnvelopeGenerator::adsrtable[16] = +{ + 0x007f, + 0x3000, + 0x1e00, + 0x0660, + 0x0182, + 0x5573, + 0x000e, + 0x3805, + 0x2424, + 0x2220, + 0x090c, + 0x0ecd, + 0x010e, + 0x23f7, + 0x5237, + 0x64a8 +}; + +void EnvelopeGenerator::reset() +{ + // counter is not changed on reset + envelope_pipeline = 0; + + state_pipeline = 0; + + attack = 0; + decay = 0; + sustain = 0; + release = 0; + + gate = false; + + resetLfsr = true; + + exponential_counter = 0; + exponential_counter_period = 1; + new_exponential_counter_period = 0; + + state = RELEASE; + counter_enabled = true; + rate = adsrtable[release]; +} + +void EnvelopeGenerator::writeCONTROL_REG(unsigned char control) +{ + const bool gate_next = (control & 0x01) != 0; + + if (gate_next != gate) + { + gate = gate_next; + + // The rate counter is never reset, thus there will be a delay before the + // envelope counter starts counting up (attack) or down (release). + + if (gate_next) + { + // Gate bit on: Start attack, decay, sustain. + next_state = ATTACK; + state_pipeline = 2; + + if (resetLfsr || (exponential_pipeline == 2)) + { + envelope_pipeline = (exponential_counter_period == 1) || (exponential_pipeline == 2) ? 2 : 4; + } + else if (exponential_pipeline == 1) + { + state_pipeline = 3; + } + } + else + { + // Gate bit off: Start release. + next_state = RELEASE; + state_pipeline = envelope_pipeline > 0 ? 3 : 2; + } + } +} + +void EnvelopeGenerator::writeATTACK_DECAY(unsigned char attack_decay) +{ + attack = (attack_decay >> 4) & 0x0f; + decay = attack_decay & 0x0f; + + if (state == ATTACK) + { + rate = adsrtable[attack]; + } + else if (state == DECAY_SUSTAIN) + { + rate = adsrtable[decay]; + } +} + +void EnvelopeGenerator::writeSUSTAIN_RELEASE(unsigned char sustain_release) +{ + // From the sustain levels it follows that both the low and high 4 bits + // of the envelope counter are compared to the 4-bit sustain value. + // This has been verified by sampling ENV3. + // + // For a detailed description see: + // http://ploguechipsounds.blogspot.it/2010/11/new-research-on-sid-adsr.html + sustain = (sustain_release & 0xf0) | ((sustain_release >> 4) & 0x0f); + + release = sustain_release & 0x0f; + + if (state == RELEASE) + { + rate = adsrtable[release]; + } +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/EnvelopeGenerator.h b/src/engine/platform/sound/c64_fp/EnvelopeGenerator.h new file mode 100644 index 00000000..f2aab387 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/EnvelopeGenerator.h @@ -0,0 +1,419 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2018 VICE Project + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef ENVELOPEGENERATOR_H +#define ENVELOPEGENERATOR_H + +#include "siddefs-fp.h" + +namespace reSIDfp +{ + +/** + * A 15 bit [LFSR] is used to implement the envelope rates, in effect dividing + * the clock to the envelope counter by the currently selected rate period. + * + * In addition, another 5 bit counter is used to implement the exponential envelope decay, + * in effect further dividing the clock to the envelope counter. + * The period of this counter is set to 1, 2, 4, 8, 16, 30 at the envelope counter + * values 255, 93, 54, 26, 14, 6, respectively. + * + * [LFSR]: https://en.wikipedia.org/wiki/Linear_feedback_shift_register + */ +class EnvelopeGenerator +{ +private: + /** + * The envelope state machine's distinct states. In addition to this, + * envelope has a hold mode, which freezes envelope counter to zero. + */ + enum State + { + ATTACK, DECAY_SUSTAIN, RELEASE + }; + +private: + /// XOR shift register for ADSR prescaling. + unsigned int lfsr; + + /// Comparison value (period) of the rate counter before next event. + unsigned int rate; + + /** + * During release mode, the SID approximates envelope decay via piecewise + * linear decay rate. + */ + unsigned int exponential_counter; + + /** + * Comparison value (period) of the exponential decay counter before next + * decrement. + */ + unsigned int exponential_counter_period; + unsigned int new_exponential_counter_period; + + unsigned int state_pipeline; + + /// + unsigned int envelope_pipeline; + + unsigned int exponential_pipeline; + + /// Current envelope state + State state; + State next_state; + + /// Whether counter is enabled. Only switching to ATTACK can release envelope. + bool counter_enabled; + + /// Gate bit + bool gate; + + /// + bool resetLfsr; + + /// The current digital value of envelope output. + unsigned char envelope_counter; + + /// Attack register + unsigned char attack; + + /// Decay register + unsigned char decay; + + /// Sustain register + unsigned char sustain; + + /// Release register + unsigned char release; + + /// The ENV3 value, sampled at the first phase of the clock + unsigned char env3; + +private: + static const unsigned int adsrtable[16]; + +private: + void set_exponential_counter(); + + void state_change(); + +public: + /** + * SID clocking. + */ + void clock(); + + /** + * Get the Envelope Generator digital output. + */ + unsigned int output() const { return envelope_counter; } + + /** + * Constructor. + */ + EnvelopeGenerator() : + lfsr(0x7fff), + rate(0), + exponential_counter(0), + exponential_counter_period(1), + new_exponential_counter_period(0), + state_pipeline(0), + envelope_pipeline(0), + exponential_pipeline(0), + state(RELEASE), + next_state(RELEASE), + counter_enabled(true), + gate(false), + resetLfsr(false), + envelope_counter(0xaa), + attack(0), + decay(0), + sustain(0), + release(0), + env3(0) + {} + + /** + * SID reset. + */ + void reset(); + + /** + * Write control register. + * + * @param control + * control register value + */ + void writeCONTROL_REG(unsigned char control); + + /** + * Write Attack/Decay register. + * + * @param attack_decay + * attack/decay value + */ + void writeATTACK_DECAY(unsigned char attack_decay); + + /** + * Write Sustain/Release register. + * + * @param sustain_release + * sustain/release value + */ + void writeSUSTAIN_RELEASE(unsigned char sustain_release); + + /** + * Return the envelope current value. + * + * @return envelope counter value + */ + unsigned char readENV() const { return env3; } +}; + +} // namespace reSIDfp + +#if RESID_INLINING || defined(ENVELOPEGENERATOR_CPP) + +namespace reSIDfp +{ + +RESID_INLINE +void EnvelopeGenerator::clock() +{ + env3 = envelope_counter; + + if (unlikely(new_exponential_counter_period > 0)) + { + exponential_counter_period = new_exponential_counter_period; + new_exponential_counter_period = 0; + } + + if (unlikely(state_pipeline)) + { + state_change(); + } + + if (unlikely(envelope_pipeline != 0) && (--envelope_pipeline == 0)) + { + if (likely(counter_enabled)) + { + if (state == ATTACK) + { + if (++envelope_counter==0xff) + { + next_state = DECAY_SUSTAIN; + state_pipeline = 3; + } + } + else if ((state == DECAY_SUSTAIN) || (state == RELEASE)) + { + if (--envelope_counter==0x00) + { + counter_enabled = false; + } + } + + set_exponential_counter(); + } + } + else if (unlikely(exponential_pipeline != 0) && (--exponential_pipeline == 0)) + { + exponential_counter = 0; + + if (((state == DECAY_SUSTAIN) && (envelope_counter != sustain)) + || (state == RELEASE)) + { + // The envelope counter can flip from 0x00 to 0xff by changing state to + // attack, then to release. The envelope counter will then continue + // counting down in the release state. + // This has been verified by sampling ENV3. + + envelope_pipeline = 1; + } + } + else if (unlikely(resetLfsr)) + { + lfsr = 0x7fff; + resetLfsr = false; + + if (state == ATTACK) + { + // The first envelope step in the attack state also resets the exponential + // counter. This has been verified by sampling ENV3. + exponential_counter = 0; // NOTE this is actually delayed one cycle, not modeled + + // The envelope counter can flip from 0xff to 0x00 by changing state to + // release, then to attack. The envelope counter is then frozen at + // zero; to unlock this situation the state must be changed to release, + // then to attack. This has been verified by sampling ENV3. + + envelope_pipeline = 2; + } + else + { + if (counter_enabled && (++exponential_counter == exponential_counter_period)) + exponential_pipeline = exponential_counter_period != 1 ? 2 : 1; + } + } + + // ADSR delay bug. + // If the rate counter comparison value is set below the current value of the + // rate counter, the counter will continue counting up until it wraps around + // to zero at 2^15 = 0x8000, and then count rate_period - 1 before the + // envelope can constly be stepped. + // This has been verified by sampling ENV3. + + // check to see if LFSR matches table value + if (likely(lfsr != rate)) + { + // it wasn't a match, clock the LFSR once + // by performing XOR on last 2 bits + const unsigned int feedback = ((lfsr << 14) ^ (lfsr << 13)) & 0x4000; + lfsr = (lfsr >> 1) | feedback; + } + else + { + resetLfsr = true; + } +} + +/** + * This is what happens on chip during state switching, + * based on die reverse engineering and transistor level + * emulation. + * + * Attack + * + * 0 - Gate on + * 1 - Counting direction changes + * During this cycle the decay rate is "accidentally" activated + * 2 - Counter is being inverted + * Now the attack rate is correctly activated + * Counter is enabled + * 3 - Counter will be counting upward from now on + * + * Decay + * + * 0 - Counter == $ff + * 1 - Counting direction changes + * The attack state is still active + * 2 - Counter is being inverted + * During this cycle the decay state is activated + * 3 - Counter will be counting downward from now on + * + * Release + * + * 0 - Gate off + * 1 - During this cycle the release state is activated if coming from sustain/decay + * *2 - Counter is being inverted, the release state is activated + * *3 - Counter will be counting downward from now on + * + * (* only if coming directly from Attack state) + * + * Freeze + * + * 0 - Counter == $00 + * 1 - Nothing + * 2 - Counter is disabled + */ +RESID_INLINE +void EnvelopeGenerator::state_change() +{ + state_pipeline--; + + switch (next_state) + { + case ATTACK: + if (state_pipeline == 1) + { + // The decay rate is "accidentally" enabled during first cycle of attack phase + rate = adsrtable[decay]; + } + else if (state_pipeline == 0) + { + state = ATTACK; + // The attack rate is correctly enabled during second cycle of attack phase + rate = adsrtable[attack]; + counter_enabled = true; + } + break; + case DECAY_SUSTAIN: + if (state_pipeline == 0) + { + state = DECAY_SUSTAIN; + rate = adsrtable[decay]; + } + break; + case RELEASE: + if (((state == ATTACK) && (state_pipeline == 0)) + || ((state == DECAY_SUSTAIN) && (state_pipeline == 1))) + { + state = RELEASE; + rate = adsrtable[release]; + } + break; + } +} + +RESID_INLINE +void EnvelopeGenerator::set_exponential_counter() +{ + // Check for change of exponential counter period. + // + // For a detailed description see: + // http://ploguechipsounds.blogspot.it/2010/03/sid-6581r3-adsr-tables-up-close.html + switch (envelope_counter) + { + case 0xff: + case 0x00: + new_exponential_counter_period = 1; + break; + + case 0x5d: + new_exponential_counter_period = 2; + break; + + case 0x36: + new_exponential_counter_period = 4; + break; + + case 0x1a: + new_exponential_counter_period = 8; + break; + + case 0x0e: + new_exponential_counter_period = 16; + break; + + case 0x06: + new_exponential_counter_period = 30; + break; + } +} + +} // namespace reSIDfp + +#endif + +#endif diff --git a/src/engine/platform/sound/c64_fp/ExternalFilter.cpp b/src/engine/platform/sound/c64_fp/ExternalFilter.cpp new file mode 100644 index 00000000..eac790b3 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/ExternalFilter.cpp @@ -0,0 +1,68 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2020 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define EXTERNALFILTER_CPP + +#include "ExternalFilter.h" + +namespace reSIDfp +{ + +/** + * Get the 3 dB attenuation point. + * + * @param res the resistance value in Ohms + * @param cap the capacitance value in Farads + */ +inline double getRC(double res, double cap) +{ + return res * cap; +} + +ExternalFilter::ExternalFilter() : + w0lp_1_s7(0), + w0hp_1_s17(0) +{ + reset(); +} + +void ExternalFilter::setClockFrequency(double frequency) +{ + const double dt = 1. / frequency; + + // Low-pass: R = 10kOhm, C = 1000pF; w0l = dt/(dt+RC) = 1e-6/(1e-6+1e4*1e-9) = 0.091 + // Cutoff 1/2*PI*RC = 1/2*PI*1e4*1e-9 = 15915.5 Hz + w0lp_1_s7 = static_cast((dt / (dt + getRC(10e3, 1000e-12))) * (1 << 7) + 0.5); + + // High-pass: R = 10kOhm, C = 10uF; w0h = dt/(dt+RC) = 1e-6/(1e-6+1e4*1e-5) = 0.00000999 + // Cutoff 1/2*PI*RC = 1/2*PI*1e4*1e-5 = 1.59155 Hz + w0hp_1_s17 = static_cast((dt / (dt + getRC(10e3, 10e-6))) * (1 << 17) + 0.5); +} + +void ExternalFilter::reset() +{ + // State of filter. + Vlp = 0; //1 << (15 + 11); + Vhp = 0; +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/ExternalFilter.h b/src/engine/platform/sound/c64_fp/ExternalFilter.h new file mode 100644 index 00000000..760ee5c2 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/ExternalFilter.h @@ -0,0 +1,125 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2020 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef EXTERNALFILTER_H +#define EXTERNALFILTER_H + +#include "siddefs-fp.h" + +namespace reSIDfp +{ + +/** + * The audio output stage in a Commodore 64 consists of two STC networks, a + * low-pass RC filter with 3 dB frequency 16kHz followed by a DC-blocker which + * acts as a high-pass filter with a cutoff dependent on the attached audio + * equipment impedance. Here we suppose an impedance of 10kOhm resulting + * in a 3 dB attenuation at 1.6Hz. + * To operate properly the 6581 audio output needs a pull-down resistor + *(1KOhm recommended, not needed on 8580) + * + * ~~~ + * 9/12V + * -----+ + * audio| 10k | + * +---o----R---o--------o-----(K) +----- + * out | | | | | |audio + * -----+ R 1k C 1000 | | 10 uF | + * | | pF +-C----o-----C-----+ 10k + * 470 | | + * GND GND pF R 1K | amp + * * * | +----- + * + * GND + * ~~~ + * + * The STC networks are connected with a [BJT] based [common collector] + * used as a voltage follower (featuring a 2SC1815 NPN transistor). + * * The C64c board additionally includes a [bootstrap] condenser to increase + * the input impedance of the common collector. + * + * [BJT]: https://en.wikipedia.org/wiki/Bipolar_junction_transistor + * [common collector]: https://en.wikipedia.org/wiki/Common_collector + * [bootstrap]: https://en.wikipedia.org/wiki/Bootstrapping_(electronics) + */ +class ExternalFilter +{ +private: + /// Lowpass filter voltage + int Vlp; + + /// Highpass filter voltage + int Vhp; + + int w0lp_1_s7; + + int w0hp_1_s17; + +public: + /** + * SID clocking. + * + * @param input + */ + int clock(unsigned short input); + + /** + * Constructor. + */ + ExternalFilter(); + + /** + * Setup of the external filter sampling parameters. + * + * @param frequency the main system clock frequency + */ + void setClockFrequency(double frequency); + + /** + * SID reset. + */ + void reset(); +}; + +} // namespace reSIDfp + +#if RESID_INLINING || defined(EXTERNALFILTER_CPP) + +namespace reSIDfp +{ + +RESID_INLINE +int ExternalFilter::clock(unsigned short input) +{ + const int Vi = (static_cast(input)<<11) - (1 << (11+15)); + const int dVlp = (w0lp_1_s7 * (Vi - Vlp) >> 7); + const int dVhp = (w0hp_1_s17 * (Vlp - Vhp) >> 17); + Vlp += dVlp; + Vhp += dVhp; + return (Vlp - Vhp) >> 11; +} + +} // namespace reSIDfp + +#endif + +#endif diff --git a/src/engine/platform/sound/c64_fp/Filter.cpp b/src/engine/platform/sound/c64_fp/Filter.cpp new file mode 100644 index 00000000..2a2dd24f --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Filter.cpp @@ -0,0 +1,90 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2013 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "Filter.h" + +namespace reSIDfp +{ + +void Filter::enable(bool enable) +{ + enabled = enable; + + if (enabled) + { + writeRES_FILT(filt); + } + else + { + filt1 = filt2 = filt3 = filtE = false; + } +} + +void Filter::reset() +{ + writeFC_LO(0); + writeFC_HI(0); + writeMODE_VOL(0); + writeRES_FILT(0); +} + +void Filter::writeFC_LO(unsigned char fc_lo) +{ + fc = (fc & 0x7f8) | (fc_lo & 0x007); + updatedCenterFrequency(); +} + +void Filter::writeFC_HI(unsigned char fc_hi) +{ + fc = (fc_hi << 3 & 0x7f8) | (fc & 0x007); + updatedCenterFrequency(); +} + +void Filter::writeRES_FILT(unsigned char res_filt) +{ + filt = res_filt; + + updateResonance((res_filt >> 4) & 0x0f); + + if (enabled) + { + filt1 = (filt & 0x01) != 0; + filt2 = (filt & 0x02) != 0; + filt3 = (filt & 0x04) != 0; + filtE = (filt & 0x08) != 0; + } + + updatedMixing(); +} + +void Filter::writeMODE_VOL(unsigned char mode_vol) +{ + vol = mode_vol & 0x0f; + lp = (mode_vol & 0x10) != 0; + bp = (mode_vol & 0x20) != 0; + hp = (mode_vol & 0x40) != 0; + voice3off = (mode_vol & 0x80) != 0; + + updatedMixing(); +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/Filter.h b/src/engine/platform/sound/c64_fp/Filter.h new file mode 100644 index 00000000..4b347336 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Filter.h @@ -0,0 +1,177 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2017 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef FILTER_H +#define FILTER_H + +namespace reSIDfp +{ + +/** + * SID filter base class + */ +class Filter +{ +protected: + /// Current volume amplifier setting. + unsigned short* currentGain; + + /// Current filter/voice mixer setting. + unsigned short* currentMixer; + + /// Filter input summer setting. + unsigned short* currentSummer; + + /// Filter resonance value. + unsigned short* currentResonance; + + /// Filter highpass state. + int Vhp; + + /// Filter bandpass state. + int Vbp; + + /// Filter lowpass state. + int Vlp; + + /// Filter external input. + int ve; + + /// Filter cutoff frequency. + unsigned int fc; + + /// Routing to filter or outside filter + bool filt1, filt2, filt3, filtE; + + /// Switch voice 3 off. + bool voice3off; + + /// Highpass, bandpass, and lowpass filter modes. + bool hp, bp, lp; + + /// Current volume. + unsigned char vol; + +private: + /// Filter enabled. + bool enabled; + + /// Selects which inputs to route through filter. + unsigned char filt; + +protected: + /** + * Set filter cutoff frequency. + */ + virtual void updatedCenterFrequency() = 0; + + /** + * Set filter resonance. + */ + virtual void updateResonance(unsigned char res) = 0; + + /** + * Mixing configuration modified (offsets change) + */ + virtual void updatedMixing() = 0; + +public: + Filter() : + currentGain(nullptr), + currentMixer(nullptr), + currentSummer(nullptr), + currentResonance(nullptr), + Vhp(0), + Vbp(0), + Vlp(0), + ve(0), + fc(0), + filt1(false), + filt2(false), + filt3(false), + filtE(false), + voice3off(false), + hp(false), + bp(false), + lp(false), + vol(0), + enabled(true), + filt(0) {} + + virtual ~Filter() {} + + /** + * SID clocking - 1 cycle + * + * @param v1 voice 1 in + * @param v2 voice 2 in + * @param v3 voice 3 in + * @return filtered output + */ + virtual unsigned short clock(int v1, int v2, int v3) = 0; + + /** + * Enable filter. + * + * @param enable + */ + void enable(bool enable); + + /** + * SID reset. + */ + void reset(); + + /** + * Write Frequency Cutoff Low register. + * + * @param fc_lo Frequency Cutoff Low-Byte + */ + void writeFC_LO(unsigned char fc_lo); + + /** + * Write Frequency Cutoff High register. + * + * @param fc_hi Frequency Cutoff High-Byte + */ + void writeFC_HI(unsigned char fc_hi); + + /** + * Write Resonance/Filter register. + * + * @param res_filt Resonance/Filter + */ + void writeRES_FILT(unsigned char res_filt); + + /** + * Write filter Mode/Volume register. + * + * @param mode_vol Filter Mode/Volume + */ + void writeMODE_VOL(unsigned char mode_vol); + + virtual void input(int input) = 0; +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/Filter6581.cpp b/src/engine/platform/sound/c64_fp/Filter6581.cpp new file mode 100644 index 00000000..c064a880 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Filter6581.cpp @@ -0,0 +1,75 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2015 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define FILTER6581_CPP + +#include "Filter6581.h" + +#include "Integrator6581.h" + +namespace reSIDfp +{ + +Filter6581::~Filter6581() +{ + delete [] f0_dac; +} + +void Filter6581::updatedCenterFrequency() +{ + const unsigned short Vw = f0_dac[fc]; + hpIntegrator->setVw(Vw); + bpIntegrator->setVw(Vw); +} + +void Filter6581::updatedMixing() +{ + currentGain = gain_vol[vol]; + + unsigned int ni = 0; + unsigned int no = 0; + + (filt1 ? ni : no)++; + (filt2 ? ni : no)++; + + if (filt3) ni++; + else if (!voice3off) no++; + + (filtE ? ni : no)++; + + currentSummer = summer[ni]; + + if (lp) no++; + if (bp) no++; + if (hp) no++; + + currentMixer = mixer[no]; +} + +void Filter6581::setFilterCurve(double curvePosition) +{ + delete [] f0_dac; + f0_dac = FilterModelConfig6581::getInstance()->getDAC(curvePosition); + updatedCenterFrequency(); +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/Filter6581.h b/src/engine/platform/sound/c64_fp/Filter6581.h new file mode 100644 index 00000000..7fca331a --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Filter6581.h @@ -0,0 +1,425 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef FILTER6581_H +#define FILTER6581_H + +#include "siddefs-fp.h" + +#include + +#include "Filter.h" +#include "FilterModelConfig6581.h" + +#include "sidcxx11.h" + +namespace reSIDfp +{ + +class Integrator6581; + +/** + * The SID filter is modeled with a two-integrator-loop biquadratic filter, + * which has been confirmed by Bob Yannes to be the actual circuit used in + * the SID chip. + * + * Measurements show that excellent emulation of the SID filter is achieved, + * except when high resonance is combined with high sustain levels. + * In this case the SID op-amps are performing less than ideally and are + * causing some peculiar behavior of the SID filter. This however seems to + * have more effect on the overall amplitude than on the color of the sound. + * + * The theory for the filter circuit can be found in "Microelectric Circuits" + * by Adel S. Sedra and Kenneth C. Smith. + * The circuit is modeled based on the explanation found there except that + * an additional inverter is used in the feedback from the bandpass output, + * allowing the summer op-amp to operate in single-ended mode. This yields + * filter outputs with levels independent of Q, which corresponds with the + * results obtained from a real SID. + * + * We have been able to model the summer and the two integrators of the circuit + * to form components of an IIR filter. + * Vhp is the output of the summer, Vbp is the output of the first integrator, + * and Vlp is the output of the second integrator in the filter circuit. + * + * According to Bob Yannes, the active stages of the SID filter are not really + * op-amps. Rather, simple NMOS inverters are used. By biasing an inverter + * into its region of quasi-linear operation using a feedback resistor from + * input to output, a MOS inverter can be made to act like an op-amp for + * small signals centered around the switching threshold. + * + * In 2008, Michael Huth facilitated closer investigation of the SID 6581 + * filter circuit by publishing high quality microscope photographs of the die. + * Tommi Lempinen has done an impressive work on re-vectorizing and annotating + * the die photographs, substantially simplifying further analysis of the + * filter circuit. + * + * The filter schematics below are reverse engineered from these re-vectorized + * and annotated die photographs. While the filter first depicted in reSID 0.9 + * is a correct model of the basic filter, the schematics are now completed + * with the audio mixer and output stage, including details on intended + * relative resistor values. Also included are schematics for the NMOS FET + * voltage controlled resistors (VCRs) used to control cutoff frequency, the + * DAC which controls the VCRs, the NMOS op-amps, and the output buffer. + * + * + * SID filter / mixer / output + * --------------------------- + * ~~~ + * +---------------------------------------------------+ + * | | + * | +--1R1-- \--+ D7 | + * | +---R1--+ | | | + * | | | o--2R1-- \--o D6 | + * | +---------o----o--Rw--o--[A>--o--Rw--o--[A>--o + * ve (EXT IN) | | | | + * D3 \ ---------------R8--o | | (CAP2A) | (CAP1A) + * | v3 | | vhp | vbp | vlp + * D2 | \ -----------R8--o +-----+ | | + * | | v2 | | | | + * D1 | | \ -------R8--o | +----------------+ | + * | | | v1 | | | | + * D0 | | | \ ---R8--+ | | +---------------------------+ + * | | | | | | | + * R6 R6 R6 R6 R6 R6 R6 + * | | | | $18 | | | $18 + * | \ | | D7: 1=open \ \ \ D6 - D4: 0=open + * | | | | | | | + * +---o---o---o-------------o---o---+ 12V + * | + * | D3 +--/ --1R2--+ | + * | +---R8--+ | | +---R2--+ | + * | | | D2 o--/ --2R2--o | | ||--+ + * +---o--[A>--o------o o--o--[A>--o--|| + * D1 o--/ --4R2--o (4.25R2) ||--+ + * $18 | | | + * 0=open D0 +--/ --8R2--+ (8.75R2) | + * + * vo (AUDIO + * OUT) + * + * + * v1 - voice 1 + * v2 - voice 2 + * v3 - voice 3 + * ve - ext in + * vhp - highpass output + * vbp - bandpass output + * vlp - lowpass output + * vo - audio out + * [A> - single ended inverting op-amp (self-biased NMOS inverter) + * Rn - "resistors", implemented with custom NMOS FETs + * Rw - cutoff frequency resistor (VCR) + * C - capacitor + * ~~~ + * Notes: + * + * R2 ~ 2.0*R1 + * R6 ~ 6.0*R1 + * R8 ~ 8.0*R1 + * R24 ~ 24.0*R1 + * + * The Rn "resistors" in the circuit are implemented with custom NMOS FETs, + * probably because of space constraints on the SID die. The silicon substrate + * is laid out in a narrow strip or "snake", with a strip length proportional + * to the intended resistance. The polysilicon gate electrode covers the entire + * silicon substrate and is fixed at 12V in order for the NMOS FET to operate + * in triode mode (a.k.a. linear mode or ohmic mode). + * + * Even in "linear mode", an NMOS FET is only an approximation of a resistor, + * as the apparant resistance increases with increasing drain-to-source + * voltage. If the drain-to-source voltage should approach the gate voltage + * of 12V, the NMOS FET will enter saturation mode (a.k.a. active mode), and + * the NMOS FET will not operate anywhere like a resistor. + * + * + * + * NMOS FET voltage controlled resistor (VCR) + * ------------------------------------------ + * ~~~ + * Vw + * + * | + * | + * R1 + * | + * +--R1--o + * | __|__ + * | ----- + * | | | + * vi -----o----+ +--o----- vo + * | | + * +----R24----+ + * + * + * vi - input + * vo - output + * Rn - "resistors", implemented with custom NMOS FETs + * Vw - voltage from 11-bit DAC (frequency cutoff control) + * ~~~ + * Notes: + * + * An approximate value for R24 can be found by using the formula for the + * filter cutoff frequency: + * + * FCmin = 1/(2*pi*Rmax*C) + * + * Assuming that a the setting for minimum cutoff frequency in combination with + * a low level input signal ensures that only negligible current will flow + * through the transistor in the schematics above, values for FCmin and C can + * be substituted in this formula to find Rmax. + * Using C = 470pF and FCmin = 220Hz (measured value), we get: + * + * FCmin = 1/(2*pi*Rmax*C) + * Rmax = 1/(2*pi*FCmin*C) = 1/(2*pi*220*470e-12) ~ 1.5MOhm + * + * From this it follows that: + * R24 = Rmax ~ 1.5MOhm + * R1 ~ R24/24 ~ 64kOhm + * R2 ~ 2.0*R1 ~ 128kOhm + * R6 ~ 6.0*R1 ~ 384kOhm + * R8 ~ 8.0*R1 ~ 512kOhm + * + * Note that these are only approximate values for one particular SID chip, + * due to process variations the values can be substantially different in + * other chips. + * + * + * + * Filter frequency cutoff DAC + * --------------------------- + * + * ~~~ + * 12V 10 9 8 7 6 5 4 3 2 1 0 VGND + * | | | | | | | | | | | | | Missing + * 2R 2R 2R 2R 2R 2R 2R 2R 2R 2R 2R 2R 2R termination + * | | | | | | | | | | | | | + * Vw --o-R-o-R-o-R-o-R-o-R-o-R-o-R-o-R-o-R-o-R-o-R-o- -+ + * + * + * Bit on: 12V + * Bit off: 5V (VGND) + * ~~~ + * As is the case with all MOS 6581 DACs, the termination to (virtual) ground + * at bit 0 is missing. + * + * Furthermore, the control of the two VCRs imposes a load on the DAC output + * which varies with the input signals to the VCRs. This can be seen from the + * VCR figure above. + * + * + * + * "Op-amp" (self-biased NMOS inverter) + * ------------------------------------ + * ~~~ + * + * 12V + * + * | + * +-----------o + * | | + * | +------o + * | | | + * | | ||--+ + * | +--|| + * | ||--+ + * ||--+ | + * vi -----|| o---o----- vo + * ||--+ | | + * | ||--+ | + * |-------|| | + * | ||--+ | + * ||--+ | | + * +--|| | | + * | ||--+ | | + * | | | | + * | +-----------o | + * | | | + * | | + * | GND | + * | | + * +----------------------+ + * + * + * vi - input + * vo - output + * ~~~ + * Notes: + * + * The schematics above are laid out to show that the "op-amp" logically + * consists of two building blocks; a saturated load NMOS inverter (on the + * right hand side of the schematics) with a buffer / bias input stage + * consisting of a variable saturated load NMOS inverter (on the left hand + * side of the schematics). + * + * Provided a reasonably high input impedance and a reasonably low output + * impedance, the "op-amp" can be modeled as a voltage transfer function + * mapping input voltage to output voltage. + * + * + * + * Output buffer (NMOS voltage follower) + * ------------------------------------- + * ~~~ + * + * 12V + * + * | + * | + * ||--+ + * vi -----|| + * ||--+ + * | + * o------ vo + * | (AUDIO + * Rext OUT) + * | + * | + * + * GND + * + * vi - input + * vo - output + * Rext - external resistor, 1kOhm + * ~~~ + * Notes: + * + * The external resistor Rext is needed to complete the NMOS voltage follower, + * this resistor has a recommended value of 1kOhm. + * + * Die photographs show that actually, two NMOS transistors are used in the + * voltage follower. However the two transistors are coupled in parallel (all + * terminals are pairwise common), which implies that we can model the two + * transistors as one. + */ +class Filter6581 final : public Filter +{ +private: + const unsigned short* f0_dac; + + unsigned short** mixer; + unsigned short** summer; + unsigned short** gain_res; + unsigned short** gain_vol; + + const int voiceScaleS11; + const int voiceDC; + + /// VCR + associated capacitor connected to highpass output. + std::unique_ptr const hpIntegrator; + + /// VCR + associated capacitor connected to bandpass output. + std::unique_ptr const bpIntegrator; + +protected: + /** + * Set filter cutoff frequency. + */ + void updatedCenterFrequency() override; + + /** + * Set filter resonance. + * + * In the MOS 6581, 1/Q is controlled linearly by res. + */ + void updateResonance(unsigned char res) override { currentResonance = gain_res[res]; } + + void updatedMixing() override; + +public: + Filter6581() : + f0_dac(FilterModelConfig6581::getInstance()->getDAC(0.5)), + mixer(FilterModelConfig6581::getInstance()->getMixer()), + summer(FilterModelConfig6581::getInstance()->getSummer()), + gain_res(FilterModelConfig6581::getInstance()->getGainRes()), + gain_vol(FilterModelConfig6581::getInstance()->getGainVol()), + voiceScaleS11(FilterModelConfig6581::getInstance()->getVoiceScaleS11()), + voiceDC(FilterModelConfig6581::getInstance()->getNormalizedVoiceDC()), + hpIntegrator(FilterModelConfig6581::getInstance()->buildIntegrator()), + bpIntegrator(FilterModelConfig6581::getInstance()->buildIntegrator()) + { + input(0); + } + + ~Filter6581(); + + unsigned short clock(int voice1, int voice2, int voice3) override; + + void input(int sample) override { ve = (sample * voiceScaleS11 * 3 >> 11) + mixer[0][0]; } + + /** + * Set filter curve type based on single parameter. + * + * @param curvePosition 0 .. 1, where 0 sets center frequency high ("light") and 1 sets it low ("dark"), default is 0.5 + */ + void setFilterCurve(double curvePosition); +}; + +} // namespace reSIDfp + +#if RESID_INLINING || defined(FILTER6581_CPP) + +#include "Integrator6581.h" + +namespace reSIDfp +{ + +RESID_INLINE +unsigned short Filter6581::clock(int voice1, int voice2, int voice3) +{ + voice1 = (voice1 * voiceScaleS11 >> 15) + voiceDC; + voice2 = (voice2 * voiceScaleS11 >> 15) + voiceDC; + // Voice 3 is silenced by voice3off if it is not routed through the filter. + voice3 = (filt3 || !voice3off) ? (voice3 * voiceScaleS11 >> 15) + voiceDC : 0; + + int Vi = 0; + int Vo = 0; + + (filt1 ? Vi : Vo) += voice1; + (filt2 ? Vi : Vo) += voice2; + (filt3 ? Vi : Vo) += voice3; + (filtE ? Vi : Vo) += ve; + + Vhp = currentSummer[currentResonance[Vbp] + Vlp + Vi]; + Vbp = hpIntegrator->solve(Vhp); + Vlp = bpIntegrator->solve(Vbp); + + if (lp) Vo += Vlp; + if (bp) Vo += Vbp; + if (hp) Vo += Vhp; + + return currentGain[currentMixer[Vo]]; +} + +} // namespace reSIDfp + +#endif + +#endif diff --git a/src/engine/platform/sound/c64_fp/Filter8580.cpp b/src/engine/platform/sound/c64_fp/Filter8580.cpp new file mode 100644 index 00000000..a70285a8 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Filter8580.cpp @@ -0,0 +1,101 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2019 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define FILTER8580_CPP + +#include "Filter8580.h" + +#include "Integrator8580.h" + +namespace reSIDfp +{ + +/** + * W/L ratio of frequency DAC bit 0, + * other bit are proportional. + * When no bit are selected a resistance with half + * W/L ratio is selected. + */ +const double DAC_WL0 = 0.00615; + +Filter8580::~Filter8580() {} + +void Filter8580::updatedCenterFrequency() +{ + double wl; + double dacWL = DAC_WL0; + if (fc) + { + wl = 0.; + for (unsigned int i = 0; i < 11; i++) + { + if (fc & (1 << i)) + { + wl += dacWL; + } + dacWL *= 2.; + } + } + else + { + wl = dacWL/2.; + } + + hpIntegrator->setFc(wl); + bpIntegrator->setFc(wl); +} + +void Filter8580::updatedMixing() +{ + currentGain = gain_vol[vol]; + + unsigned int ni = 0; + unsigned int no = 0; + + (filt1 ? ni : no)++; + (filt2 ? ni : no)++; + + if (filt3) ni++; + else if (!voice3off) no++; + + (filtE ? ni : no)++; + + currentSummer = summer[ni]; + + if (lp) no++; + if (bp) no++; + if (hp) no++; + + currentMixer = mixer[no]; +} + +void Filter8580::setFilterCurve(double curvePosition) +{ + // Adjust cp + // 1.2 <= cp <= 1.8 + cp = 1.8 - curvePosition * 3./5.; + + hpIntegrator->setV(cp); + bpIntegrator->setV(cp); +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/Filter8580.h b/src/engine/platform/sound/c64_fp/Filter8580.h new file mode 100644 index 00000000..2166ec0d --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Filter8580.h @@ -0,0 +1,383 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef FILTER8580_H +#define FILTER8580_H + +#include "siddefs-fp.h" + +#include + +#include "Filter.h" +#include "FilterModelConfig8580.h" +#include "Integrator8580.h" + +#include "sidcxx11.h" + +namespace reSIDfp +{ + +class Integrator8580; + +/** + * Filter for 8580 chip + * -------------------- + * The 8580 filter stage had been redesigned to be more linear and robust + * against temperature change. It also features real op-amps and a + * revisited resonance model. + * The filter schematics below are reverse engineered from re-vectorized + * and annotated die photographs. Credits to Michael Huth for the microscope + * photographs of the die, Tommi Lempinen for re-vectorizating and annotating + * the images and ttlworks from forum.6502.org for the circuit analysis. + * + * ~~~ + * + * +---------------------------------------------------+ + * | $17 +----Rf-+ | + * | | | | + * | D4&!D5 o- \-R3-o | + * | | | $17 | + * | !D4&!D5 o- \-R2-o | + * | | | +---R8-- \--+ !D6&D7 | + * | D4&!D5 o- \-R1-o | | | + * | | | o---RC-- \--o D6&D7 | + * | +---------o----o--Rfc-o--[A>--o--Rfc-o--[A>--o + * ve (EXT IN) | | | | + * D3 \ --------------R12--o | | (CAP2A) | (CAP1A) + * | v3 | | vhp | vbp | vlp + * D2 | \ -----------R7--o +-----+ | | + * | | v2 | | | | + * D1 | | \ -------R7--o | +----------------+ | + * | | | v1 | | | | + * D0 | | | \ ---R7--+ | | +---------------------------+ + * | | | | | | | + * R9 R5 R5 R5 R5 R5 R5 + * | | | | $18 | | | $18 + * | \ | | D7: 1=open \ \ \ D6 - D4: 0=open + * | | | | | | | + * +---o---o---o-------------o---o---+ + * | + * | D3 +--/ --1R4--+ + * | +---R8--+ | | +---R2--+ + * | | | D2 o--/ --2R4--o | | + * +---o--[A>--o------o o--o--[A>--o-- vo (AUDIO OUT) + * D1 o--/ --4R4--o + * $18 | | + * 0=open D0 +--/ --8R4--+ + * + * + * + * Resonance + * --------- + * For resonance, we have two tiny DACs that controls both the input + * and feedback resistances. + * + * The "resistors" are switched in as follows by bits in register $17: + * + * feedback: + * R1: bit4&!bit5 + * R2: !bit4&bit5 + * R3: bit4&bit5 + * Rf: always on + * + * input: + * R4: bit6&!bit7 + * R8: !bit6&bit7 + * RC: bit6&bit7 + * Ri: !(R4|R8|RC) = !(bit6|bit7) = !bit6&!bit7 + * + * + * The relative "resistor" values are approximately (using channel length): + * + * R1 = 15.3*Ri + * R2 = 7.3*Ri + * R3 = 4.7*Ri + * Rf = 1.4*Ri + * R4 = 1.4*Ri + * R8 = 2.0*Ri + * RC = 2.8*Ri + * + * + * Approximate values for 1/Q can now be found as follows (assuming an + * ideal op-amp): + * + * res feedback input -gain (1/Q) + * --- -------- ----- ---------- + * 0 Rf Ri Rf/Ri = 1/(Ri*(1/Rf)) = 1/0.71 + * 1 Rf|R1 Ri (Rf|R1)/Ri = 1/(Ri*(1/Rf+1/R1)) = 1/0.78 + * 2 Rf|R2 Ri (Rf|R2)/Ri = 1/(Ri*(1/Rf+1/R2)) = 1/0.85 + * 3 Rf|R3 Ri (Rf|R3)/Ri = 1/(Ri*(1/Rf+1/R3)) = 1/0.92 + * 4 Rf R4 Rf/R4 = 1/(R4*(1/Rf)) = 1/1.00 + * 5 Rf|R1 R4 (Rf|R1)/R4 = 1/(R4*(1/Rf+1/R1)) = 1/1.10 + * 6 Rf|R2 R4 (Rf|R2)/R4 = 1/(R4*(1/Rf+1/R2)) = 1/1.20 + * 7 Rf|R3 R4 (Rf|R3)/R4 = 1/(R4*(1/Rf+1/R3)) = 1/1.30 + * 8 Rf R8 Rf/R8 = 1/(R8*(1/Rf)) = 1/1.43 + * 9 Rf|R1 R8 (Rf|R1)/R8 = 1/(R8*(1/Rf+1/R1)) = 1/1.56 + * A Rf|R2 R8 (Rf|R2)/R8 = 1/(R8*(1/Rf+1/R2)) = 1/1.70 + * B Rf|R3 R8 (Rf|R3)/R8 = 1/(R8*(1/Rf+1/R3)) = 1/1.86 + * C Rf RC Rf/RC = 1/(RC*(1/Rf)) = 1/2.00 + * D Rf|R1 RC (Rf|R1)/RC = 1/(RC*(1/Rf+1/R1)) = 1/2.18 + * E Rf|R2 RC (Rf|R2)/RC = 1/(RC*(1/Rf+1/R2)) = 1/2.38 + * F Rf|R3 RC (Rf|R3)/RC = 1/(RC*(1/Rf+1/R3)) = 1/2.60 + * + * + * These data indicate that the following function for 1/Q has been + * modeled in the MOS 8580: + * + * 1/Q = 2^(1/2)*2^(-x/8) = 2^(1/2 - x/8) = 2^((4 - x)/8) + * + * + * + * Op-amps + * ------- + * Unlike the 6581, the 8580 has real OpAmps. + * + * Temperature compensated differential amplifier: + * + * 9V + * + * | + * +-------o-o-o-------+ + * | | | | + * | R R | + * +--|| | | ||--+ + * ||---o o---|| + * +--|| | | ||--+ + * | | | | + * o-----+ | | o--- Va + * | | | | | + * +--|| | | | ||--+ + * ||-o-+---+---|| + * +--|| | | ||--+ + * | | | | + * | | + * GND | | GND + * ||--+ +--|| + * in- -----|| ||------ in+ + * ||----o----|| + * | + * 8 Current sink + * | + * + * GND + * + * Inverter + non-inverting output amplifier: + * + * Va ---o---||-------------------o--------------------+ + * | | 9V | + * | +----------+----------+ | | + * | 9V | | 9V | ||--+ | + * | | | 9V | | +-|| | + * | R | | | ||--+ ||--+ | + * | | | ||--+ +--|| o---o--- Vout + * | o---o---|| ||--+ ||--+ + * | | ||--+ o-----|| + * | ||--+ | ||--+ ||--+ + * +-----|| o-----|| | + * ||--+ | ||--+ + * | R | GND + * | + * GND GND + * GND + * + * + * + * Virtual ground + * -------------- + * A PolySi resitive voltage divider provides the voltage + * for the positive input of the filter op-amps. + * + * 5V + * +----------+ + * | | |\ | + * R1 +---|-\ | + * 5V | |A >---o--- Vref + * o-------|+/ + * | | |/ + * R10 R4 + * | | + * o---+ + * | + * R10 + * | + * + * GND + * + * Rn = n*R1 + * + * + * + * Rfc - freq control DAC resistance ladder + * ---------------------------------------- + * The 8580 has 11 bits for frequency control, but 12 bit DACs. + * If those 11 bits would be '0', the impedance of the DACs would be "infinitely high". + * To get around this, there is an 11 input NOR gate below the DACs sensing those 11 bits. + * If all are 0, the NOR gate gives the gate control voltage to the 12 bit DAC LSB. + * + * ----o---o--...--o---o---o--- + * | | | | | + * Rb10 Rb9 ... Rb1 Rb0 R0 + * | | | | | + * ----o---o--...--o---o---o--- + * + * + * + * Crystal stabilized precision switched capacitor voltage divider + * --------------------------------------------------------------- + * There is a FET working as a temperature sensor close to the DACs which changes the gate voltage + * of the frequency control DACs according to the temperature of the DACs, + * to reduce the effects of temperature on the filter curve. + * An asynchronous 3 bit binary counter, running at the speed of PHI2, drives two big capacitors + * whose AC resistance is then used as a voltage divider. + * This implicates that frequency difference between PAL and NTSC might shift the filter curve by 4% or such. + * + * |\ OpAmp has a smaller capacitor than the other OPs + * Vref ---|+\ + * |A >---o--- Vdac + * +-------|-/ | + * | |/ | + * | | + * C1 | C2 | + * +---||---o---+ +---o-----||-------o + * | | | | | | + * o----+ | ----- | | + * | | | ----- +----+ +-----o + * | ----- | | | | + * | ----- | ----- | + * | | | ----- | + * | +-----------+ | | + * | /Q Q | +-------+ + * GND +-----------+ FET close to DAC + * | clk/8 | working as temperature sensor + * +-----------+ + */ +class Filter8580 final : public Filter +{ +private: + unsigned short** mixer; + unsigned short** summer; + unsigned short** gain_res; + unsigned short** gain_vol; + + const int voiceScaleS11; + const int voiceDC; + + double cp; + + /// VCR + associated capacitor connected to highpass output. + std::unique_ptr const hpIntegrator; + + /// VCR + associated capacitor connected to bandpass output. + std::unique_ptr const bpIntegrator; + +protected: + /** + * Set filter cutoff frequency. + */ + void updatedCenterFrequency() override; + + /** + * Set filter resonance. + * + * @param res the new resonance value + */ + void updateResonance(unsigned char res) override { currentResonance = gain_res[res]; } + + void updatedMixing() override; + +public: + Filter8580() : + mixer(FilterModelConfig8580::getInstance()->getMixer()), + summer(FilterModelConfig8580::getInstance()->getSummer()), + gain_res(FilterModelConfig8580::getInstance()->getGainRes()), + gain_vol(FilterModelConfig8580::getInstance()->getGainVol()), + voiceScaleS11(FilterModelConfig8580::getInstance()->getVoiceScaleS11()), + voiceDC(FilterModelConfig8580::getInstance()->getNormalizedVoiceDC()), + cp(0.5), + hpIntegrator(FilterModelConfig8580::getInstance()->buildIntegrator()), + bpIntegrator(FilterModelConfig8580::getInstance()->buildIntegrator()) + { + setFilterCurve(cp); + input(0); + } + + ~Filter8580(); + + unsigned short clock(int voice1, int voice2, int voice3) override; + + void input(int sample) override { ve = (sample * voiceScaleS11 * 3 >> 11) + mixer[0][0]; } + + /** + * Set filter curve type based on single parameter. + * + * @param curvePosition 0 .. 1, where 0 sets center frequency high ("light") and 1 sets it low ("dark"), default is 0.5 + */ + void setFilterCurve(double curvePosition); +}; + +} // namespace reSIDfp + +#if RESID_INLINING || defined(FILTER8580_CPP) + +namespace reSIDfp +{ + +RESID_INLINE +unsigned short Filter8580::clock(int voice1, int voice2, int voice3) +{ + voice1 = (voice1 * voiceScaleS11 >> 15) + voiceDC; + voice2 = (voice2 * voiceScaleS11 >> 15) + voiceDC; + // Voice 3 is silenced by voice3off if it is not routed through the filter. + voice3 = (filt3 || !voice3off) ? (voice3 * voiceScaleS11 >> 15) + voiceDC : 0; + + int Vi = 0; + int Vo = 0; + + (filt1 ? Vi : Vo) += voice1; + (filt2 ? Vi : Vo) += voice2; + (filt3 ? Vi : Vo) += voice3; + (filtE ? Vi : Vo) += ve; + + Vhp = currentSummer[currentResonance[Vbp] + Vlp + Vi]; + Vbp = hpIntegrator->solve(Vhp); + Vlp = bpIntegrator->solve(Vbp); + + if (lp) Vo += Vlp; + if (bp) Vo += Vbp; + if (hp) Vo += Vhp; + + return currentGain[currentMixer[Vo]]; +} + +} // namespace reSIDfp + +#endif + +#endif diff --git a/src/engine/platform/sound/c64_fp/FilterModelConfig.cpp b/src/engine/platform/sound/c64_fp/FilterModelConfig.cpp new file mode 100644 index 00000000..8fb76238 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/FilterModelConfig.cpp @@ -0,0 +1,79 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "FilterModelConfig.h" + +#include + +namespace reSIDfp +{ + +FilterModelConfig::FilterModelConfig( + double vvr, + double vdv, + double c, + double vdd, + double vth, + double ucox, + const Spline::Point *opamp_voltage, + int opamp_size +) : + voice_voltage_range(vvr), + voice_DC_voltage(vdv), + C(c), + Vdd(vdd), + Vth(vth), + Ut(26.0e-3), + uCox(ucox), + Vddt(Vdd - Vth), + vmin(opamp_voltage[0].x), + vmax(std::max(Vddt, opamp_voltage[0].y)), + denorm(vmax - vmin), + norm(1.0 / denorm), + N16(norm * ((1 << 16) - 1)), + currFactorCoeff(denorm * (uCox / 2. * 1.0e-6 / C)) +{ + // Convert op-amp voltage transfer to 16 bit values. + + std::vector scaled_voltage(opamp_size); + + for (int i = 0; i < opamp_size; i++) + { + scaled_voltage[i].x = N16 * (opamp_voltage[i].x - opamp_voltage[i].y + denorm) / 2.; + scaled_voltage[i].y = N16 * (opamp_voltage[i].x - vmin); + } + + // Create lookup table mapping capacitor voltage to op-amp input voltage: + + Spline s(scaled_voltage); + + for (int x = 0; x < (1 << 16); x++) + { + const Spline::Point out = s.evaluate(x); + // If Vmax > max opamp_voltage the first elements may be negative + double tmp = out.x > 0. ? out.x : 0.; + assert(tmp < 65535.5); + opamp_rev[x] = static_cast(tmp + 0.5); + } +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/FilterModelConfig.h b/src/engine/platform/sound/c64_fp/FilterModelConfig.h new file mode 100644 index 00000000..d8ae77ab --- /dev/null +++ b/src/engine/platform/sound/c64_fp/FilterModelConfig.h @@ -0,0 +1,166 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef FILTERMODELCONFIG_H +#define FILTERMODELCONFIG_H + +#include +#include + +#include "Spline.h" + +#include "sidcxx11.h" + +namespace reSIDfp +{ + +class FilterModelConfig +{ +protected: + const double voice_voltage_range; + const double voice_DC_voltage; + + /// Capacitor value. + const double C; + + /// Transistor parameters. + //@{ + const double Vdd; + const double Vth; ///< Threshold voltage + const double Ut; ///< Thermal voltage: Ut = kT/q = 8.61734315e-5*T ~ 26mV + const double uCox; ///< Transconductance coefficient: u*Cox + const double Vddt; ///< Vdd - Vth + //@} + + // Derived stuff + const double vmin, vmax; + const double denorm, norm; + + /// Fixed point scaling for 16 bit op-amp output. + const double N16; + + /// Current factor coefficient for op-amp integrators. + const double currFactorCoeff; + + /// Lookup tables for gain and summer op-amps in output stage / filter. + //@{ + unsigned short* mixer[8]; //-V730_NOINIT this is initialized in the derived class constructor + unsigned short* summer[5]; //-V730_NOINIT this is initialized in the derived class constructor + unsigned short* gain_vol[16]; //-V730_NOINIT this is initialized in the derived class constructor + unsigned short* gain_res[16]; //-V730_NOINIT this is initialized in the derived class constructor + //@} + + /// Reverse op-amp transfer function. + unsigned short opamp_rev[1 << 16]; //-V730_NOINIT this is initialized in the derived class constructor + +private: + FilterModelConfig (const FilterModelConfig&) DELETE; + FilterModelConfig& operator= (const FilterModelConfig&) DELETE; + +protected: + /** + * @param vvr voice voltage range + * @param vdv voice DC voltage + * @param c capacitor value + * @param vdd Vdd + * @param vth threshold voltage + * @param ucox u*Cox + * @param ominv opamp min voltage + * @param omaxv opamp max voltage + */ + FilterModelConfig( + double vvr, + double vdv, + double c, + double vdd, + double vth, + double ucox, + const Spline::Point *opamp_voltage, + int opamp_size + ); + + ~FilterModelConfig() + { + for (int i = 0; i < 8; i++) + { + delete [] mixer[i]; + } + + for (int i = 0; i < 5; i++) + { + delete [] summer[i]; + } + + for (int i = 0; i < 16; i++) + { + delete [] gain_vol[i]; + delete [] gain_res[i]; + } + } + +public: + unsigned short** getGainVol() { return gain_vol; } + unsigned short** getGainRes() { return gain_res; } + unsigned short** getSummer() { return summer; } + unsigned short** getMixer() { return mixer; } + + /** + * The digital range of one voice is 20 bits; create a scaling term + * for multiplication which fits in 11 bits. + */ + int getVoiceScaleS11() const { return static_cast((norm * ((1 << 11) - 1)) * voice_voltage_range); } + + /** + * The "zero" output level of the voices. + */ + int getNormalizedVoiceDC() const { return static_cast(N16 * (voice_DC_voltage - vmin)); } + + inline unsigned short getOpampRev(int i) const { return opamp_rev[i]; } + inline double getVddt() const { return Vddt; } + inline double getVth() const { return Vth; } + inline double getVoiceDCVoltage() const { return voice_DC_voltage; } + + // helper functions + inline unsigned short getNormalizedValue(double value) const + { + const double tmp = N16 * (value - vmin); + assert(tmp > -0.5 && tmp < 65535.5); + return static_cast(tmp + 0.5); + } + + inline unsigned short getNormalizedCurrentFactor(double wl) const + { + const double tmp = (1 << 13) * currFactorCoeff * wl; + assert(tmp > -0.5 && tmp < 65535.5); + return static_cast(tmp + 0.5); + } + + inline unsigned short getNVmin() const { + const double tmp = N16 * vmin; + assert(tmp > -0.5 && tmp < 65535.5); + return static_cast(tmp + 0.5); + } +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/FilterModelConfig6581.cpp b/src/engine/platform/sound/c64_fp/FilterModelConfig6581.cpp new file mode 100644 index 00000000..3d86bdcf --- /dev/null +++ b/src/engine/platform/sound/c64_fp/FilterModelConfig6581.cpp @@ -0,0 +1,263 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "FilterModelConfig6581.h" + +#include + +#include "Integrator6581.h" +#include "OpAmp.h" + +namespace reSIDfp +{ + +#ifndef HAVE_CXX11 +/** + * Compute log(1+x) without losing precision for small values of x + * + * @note when compiling with -ffastm-math the compiler will + * optimize the expression away leaving a plain log(1. + x) + */ +inline double log1p(double x) +{ + return log(1. + x) - (((1. + x) - 1.) - x) / (1. + x); +} +#endif + +const unsigned int OPAMP_SIZE = 33; + +/** + * This is the SID 6581 op-amp voltage transfer function, measured on + * CAP1B/CAP1A on a chip marked MOS 6581R4AR 0687 14. + * All measured chips have op-amps with output voltages (and thus input + * voltages) within the range of 0.81V - 10.31V. + */ +const Spline::Point opamp_voltage[OPAMP_SIZE] = +{ + { 0.81, 10.31 }, // Approximate start of actual range + { 2.40, 10.31 }, + { 2.60, 10.30 }, + { 2.70, 10.29 }, + { 2.80, 10.26 }, + { 2.90, 10.17 }, + { 3.00, 10.04 }, + { 3.10, 9.83 }, + { 3.20, 9.58 }, + { 3.30, 9.32 }, + { 3.50, 8.69 }, + { 3.70, 8.00 }, + { 4.00, 6.89 }, + { 4.40, 5.21 }, + { 4.54, 4.54 }, // Working point (vi = vo) + { 4.60, 4.19 }, + { 4.80, 3.00 }, + { 4.90, 2.30 }, // Change of curvature + { 4.95, 2.03 }, + { 5.00, 1.88 }, + { 5.05, 1.77 }, + { 5.10, 1.69 }, + { 5.20, 1.58 }, + { 5.40, 1.44 }, + { 5.60, 1.33 }, + { 5.80, 1.26 }, + { 6.00, 1.21 }, + { 6.40, 1.12 }, + { 7.00, 1.02 }, + { 7.50, 0.97 }, + { 8.50, 0.89 }, + { 10.00, 0.81 }, + { 10.31, 0.81 }, // Approximate end of actual range +}; + +std::unique_ptr FilterModelConfig6581::instance(nullptr); + +FilterModelConfig6581* FilterModelConfig6581::getInstance() +{ + if (!instance.get()) + { + instance.reset(new FilterModelConfig6581()); + } + + return instance.get(); +} + +FilterModelConfig6581::FilterModelConfig6581() : + FilterModelConfig( + 1.5, // voice voltage range + 5.075, // voice DC voltage + 470e-12, // capacitor value + 12.18, // Vdd + 1.31, // Vth + 20e-6, // uCox + opamp_voltage, + OPAMP_SIZE + ), + WL_vcr(9.0 / 1.0), + WL_snake(1.0 / 115.0), + dac_zero(6.65), + dac_scale(2.63), + dac(DAC_BITS) +{ + dac.kinkedDac(MOS6581); + + // Create lookup tables for gains / summers. + + OpAmp opampModel(std::vector(std::begin(opamp_voltage), std::end(opamp_voltage)), Vddt); + + // The filter summer operates at n ~ 1, and has 5 fundamentally different + // input configurations (2 - 6 input "resistors"). + // + // Note that all "on" transistors are modeled as one. This is not + // entirely accurate, since the input for each transistor is different, + // and transistors are not linear components. However modeling all + // transistors separately would be extremely costly. + for (int i = 0; i < 5; i++) + { + const int idiv = 2 + i; // 2 - 6 input "resistors". + const int size = idiv << 16; + const double n = idiv; + opampModel.reset(); + summer[i] = new unsigned short[size]; + + for (int vi = 0; vi < size; vi++) + { + const double vin = vmin + vi / N16 / idiv; /* vmin .. vmax */ + summer[i][vi] = getNormalizedValue(opampModel.solve(n, vin)); + } + } + + // The audio mixer operates at n ~ 8/6, and has 8 fundamentally different + // input configurations (0 - 7 input "resistors"). + // + // All "on", transistors are modeled as one - see comments above for + // the filter summer. + for (int i = 0; i < 8; i++) + { + const int idiv = (i == 0) ? 1 : i; + const int size = (i == 0) ? 1 : i << 16; + const double n = i * 8.0 / 6.0; + opampModel.reset(); + mixer[i] = new unsigned short[size]; + + for (int vi = 0; vi < size; vi++) + { + const double vin = vmin + vi / N16 / idiv; /* vmin .. vmax */ + mixer[i][vi] = getNormalizedValue(opampModel.solve(n, vin)); + } + } + + // 4 bit "resistor" ladders in the audio + // output gain necessitate 16 gain tables. + // From die photographs of the bandpass and volume "resistor" ladders + // it follows that gain ~ vol/12 (assuming ideal + // op-amps and ideal "resistors"). + for (int n8 = 0; n8 < 16; n8++) + { + const int size = 1 << 16; + const double n = n8 / 12.0; + opampModel.reset(); + gain_vol[n8] = new unsigned short[size]; + + for (int vi = 0; vi < size; vi++) + { + const double vin = vmin + vi / N16; /* vmin .. vmax */ + gain_vol[n8][vi] = getNormalizedValue(opampModel.solve(n, vin)); + } + } + + // 4 bit "resistor" ladders in the bandpass resonance gain + // necessitate 16 gain tables. + // From die photographs of the bandpass and volume "resistor" ladders + // it follows that 1/Q ~ ~res/8 (assuming ideal + // op-amps and ideal "resistors"). + for (int n8 = 0; n8 < 16; n8++) + { + const int size = 1 << 16; + const double n = (~n8 & 0xf) / 8.0; + opampModel.reset(); + gain_res[n8] = new unsigned short[size]; + + for (int vi = 0; vi < size; vi++) + { + const double vin = vmin + vi / N16; /* vmin .. vmax */ + gain_res[n8][vi] = getNormalizedValue(opampModel.solve(n, vin)); + } + } + + const double nVddt = N16 * (Vddt - vmin); + + for (unsigned int i = 0; i < (1 << 16); i++) + { + // The table index is right-shifted 16 times in order to fit in + // 16 bits; the argument to sqrt is thus multiplied by (1 << 16). + const double tmp = nVddt - sqrt(static_cast(i << 16)); + assert(tmp > -0.5 && tmp < 65535.5); + vcr_nVg[i] = static_cast(tmp + 0.5); + } + + // EKV model: + // + // Ids = Is * (if - ir) + // Is = (2 * u*Cox * Ut^2)/k * W/L + // if = ln^2(1 + e^((k*(Vg - Vt) - Vs)/(2*Ut)) + // ir = ln^2(1 + e^((k*(Vg - Vt) - Vd)/(2*Ut)) + + // moderate inversion characteristic current + const double Is = (2. * uCox * Ut * Ut) * WL_vcr; + + // Normalized current factor for 1 cycle at 1MHz. + const double N15 = norm * ((1 << 15) - 1); + const double n_Is = N15 * 1.0e-6 / C * Is; + + // kVgt_Vx = k*(Vg - Vt) - Vx + // I.e. if k != 1.0, Vg must be scaled accordingly. + for (int kVgt_Vx = 0; kVgt_Vx < (1 << 16); kVgt_Vx++) + { + const double log_term = log1p(exp((kVgt_Vx / N16) / (2. * Ut))); + // Scaled by m*2^15 + const double tmp = n_Is * log_term * log_term; + assert(tmp > -0.5 && tmp < 65535.5); + vcr_n_Ids_term[kVgt_Vx] = static_cast(tmp + 0.5); + } +} + +unsigned short* FilterModelConfig6581::getDAC(double adjustment) const +{ + const double dac_zero = getDacZero(adjustment); + + unsigned short* f0_dac = new unsigned short[1 << DAC_BITS]; + + for (unsigned int i = 0; i < (1 << DAC_BITS); i++) + { + const double fcd = dac.getOutput(i); + f0_dac[i] = getNormalizedValue(dac_zero + fcd * dac_scale / (1 << DAC_BITS)); + } + + return f0_dac; +} + +std::unique_ptr FilterModelConfig6581::buildIntegrator() +{ + return MAKE_UNIQUE(Integrator6581, this, WL_snake); +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/FilterModelConfig6581.h b/src/engine/platform/sound/c64_fp/FilterModelConfig6581.h new file mode 100644 index 00000000..85cbd43f --- /dev/null +++ b/src/engine/platform/sound/c64_fp/FilterModelConfig6581.h @@ -0,0 +1,112 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2020 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef FILTERMODELCONFIG6581_H +#define FILTERMODELCONFIG6581_H + +#include "FilterModelConfig.h" + +#include + +#include "Dac.h" + +#include "sidcxx14.h" + +namespace reSIDfp +{ + +class Integrator6581; + +/** + * Calculate parameters for 6581 filter emulation. + */ +class FilterModelConfig6581 final : public FilterModelConfig +{ +private: + static const unsigned int DAC_BITS = 11; + +private: + static std::unique_ptr instance; + // This allows access to the private constructor +#ifdef HAVE_CXX11 + friend std::unique_ptr::deleter_type; +#else + friend class std::auto_ptr; +#endif + + /// Transistor parameters. + //@{ + const double WL_vcr; ///< W/L for VCR + const double WL_snake; ///< W/L for "snake" + //@} + + /// DAC parameters. + //@{ + const double dac_zero; + const double dac_scale; + //@} + + /// DAC lookup table + Dac dac; + + /// VCR - 6581 only. + //@{ + unsigned short vcr_nVg[1 << 16]; + unsigned short vcr_n_Ids_term[1 << 16]; + //@} + +private: + double getDacZero(double adjustment) const { return dac_zero + (1. - adjustment); } + + FilterModelConfig6581(); + ~FilterModelConfig6581() DEFAULT; + +public: + static FilterModelConfig6581* getInstance(); + + /** + * Construct an 11 bit cutoff frequency DAC output voltage table. + * Ownership is transferred to the requester which becomes responsible + * of freeing the object when done. + * + * @param adjustment + * @return the DAC table + */ + unsigned short* getDAC(double adjustment) const; + + /** + * Construct an integrator solver. + * + * @return the integrator + */ + std::unique_ptr buildIntegrator(); + + inline unsigned short getVcr_nVg(int i) const { return vcr_nVg[i]; } + inline unsigned short getVcr_n_Ids_term(int i) const { return vcr_n_Ids_term[i]; } + // only used if SLOPE_FACTOR is defined + inline double getUt() const { return Ut; } + inline double getN16() const { return N16; } +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/FilterModelConfig8580.cpp b/src/engine/platform/sound/c64_fp/FilterModelConfig8580.cpp new file mode 100644 index 00000000..fd2a16fa --- /dev/null +++ b/src/engine/platform/sound/c64_fp/FilterModelConfig8580.cpp @@ -0,0 +1,222 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2020 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "FilterModelConfig8580.h" + +#include "Integrator8580.h" +#include "OpAmp.h" + +namespace reSIDfp +{ + +/* + * R1 = 15.3*Ri + * R2 = 7.3*Ri + * R3 = 4.7*Ri + * Rf = 1.4*Ri + * R4 = 1.4*Ri + * R8 = 2.0*Ri + * RC = 2.8*Ri + * + * res feedback input + * --- -------- ----- + * 0 Rf Ri + * 1 Rf|R1 Ri + * 2 Rf|R2 Ri + * 3 Rf|R3 Ri + * 4 Rf R4 + * 5 Rf|R1 R4 + * 6 Rf|R2 R4 + * 7 Rf|R3 R4 + * 8 Rf R8 + * 9 Rf|R1 R8 + * A Rf|R2 R8 + * B Rf|R3 R8 + * C Rf RC + * D Rf|R1 RC + * E Rf|R2 RC + * F Rf|R3 RC + */ +const double resGain[16] = +{ + 1.4/1.0, // Rf/Ri 1.4 + ((1.4*15.3)/(1.4+15.3))/1.0, // (Rf|R1)/Ri 1.28263 + ((1.4*7.3)/(1.4+7.3))/1.0, // (Rf|R2)/Ri 1.17471 + ((1.4*4.7)/(1.4+4.7))/1.0, // (Rf|R3)/Ri 1.07869 + 1.4/1.4, // Rf/R4 1 + ((1.4*15.3)/(1.4+15.3))/1.4, // (Rf|R1)/R4 0.916168 + ((1.4*7.3)/(1.4+7.3))/1.4, // (Rf|R2)/R4 0.83908 + ((1.4*4.7)/(1.4+4.7))/1.4, // (Rf|R3)/R4 0.770492 + 1.4/2.0, // Rf/R8 0.7 + ((1.4*15.3)/(1.4+15.3))/2.0, // (Rf|R1)/R8 0.641317 + ((1.4*7.3)/(1.4+7.3))/2.0, // (Rf|R2)/R8 0.587356 + ((1.4*4.7)/(1.4+4.7))/2.0, // (Rf|R3)/R8 0.539344 + 1.4/2.8, // Rf/RC 0.5 + ((1.4*15.3)/(1.4+15.3))/2.8, // (Rf|R1)/RC 0.458084 + ((1.4*7.3)/(1.4+7.3))/2.8, // (Rf|R2)/RC 0.41954 + ((1.4*4.7)/(1.4+4.7))/2.8, // (Rf|R3)/RC 0.385246 +}; + +const unsigned int OPAMP_SIZE = 21; + +/** + * This is the SID 8580 op-amp voltage transfer function, measured on + * CAP1B/CAP1A on a chip marked CSG 8580R5 1690 25. + */ +const Spline::Point opamp_voltage[OPAMP_SIZE] = +{ + { 1.30, 8.91 }, // Approximate start of actual range + { 4.76, 8.91 }, + { 4.77, 8.90 }, + { 4.78, 8.88 }, + { 4.785, 8.86 }, + { 4.79, 8.80 }, + { 4.795, 8.60 }, + { 4.80, 8.25 }, + { 4.805, 7.50 }, + { 4.81, 6.10 }, + { 4.815, 4.05 }, // Change of curvature + { 4.82, 2.27 }, + { 4.825, 1.65 }, + { 4.83, 1.55 }, + { 4.84, 1.47 }, + { 4.85, 1.43 }, + { 4.87, 1.37 }, + { 4.90, 1.34 }, + { 5.00, 1.30 }, + { 5.10, 1.30 }, + { 8.91, 1.30 }, // Approximate end of actual range +}; + +std::unique_ptr FilterModelConfig8580::instance(nullptr); + +FilterModelConfig8580* FilterModelConfig8580::getInstance() +{ + if (!instance.get()) + { + instance.reset(new FilterModelConfig8580()); + } + + return instance.get(); +} + +FilterModelConfig8580::FilterModelConfig8580() : + FilterModelConfig( + 0.25, // voice voltage range FIXME measure + 4.80, // voice DC voltage FIXME was 4.76 + 22e-9, // capacitor value + 9.09, // Vdd + 0.80, // Vth + 100e-6, // uCox + opamp_voltage, + OPAMP_SIZE + ) +{ + // Create lookup tables for gains / summers. + + OpAmp opampModel(std::vector(std::begin(opamp_voltage), std::end(opamp_voltage)), Vddt); + + // The filter summer operates at n ~ 1, and has 5 fundamentally different + // input configurations (2 - 6 input "resistors"). + // + // Note that all "on" transistors are modeled as one. This is not + // entirely accurate, since the input for each transistor is different, + // and transistors are not linear components. However modeling all + // transistors separately would be extremely costly. + for (int i = 0; i < 5; i++) + { + const int idiv = 2 + i; // 2 - 6 input "resistors". + const int size = idiv << 16; + const double n = idiv; + opampModel.reset(); + summer[i] = new unsigned short[size]; + + for (int vi = 0; vi < size; vi++) + { + const double vin = vmin + vi / N16 / idiv; /* vmin .. vmax */ + summer[i][vi] = getNormalizedValue(opampModel.solve(n, vin)); + } + } + + // The audio mixer operates at n ~ 8/5, and has 8 fundamentally different + // input configurations (0 - 7 input "resistors"). + // + // All "on", transistors are modeled as one - see comments above for + // the filter summer. + for (int i = 0; i < 8; i++) + { + const int idiv = (i == 0) ? 1 : i; + const int size = (i == 0) ? 1 : i << 16; + const double n = i * 8.0 / 5.0; + opampModel.reset(); + mixer[i] = new unsigned short[size]; + + for (int vi = 0; vi < size; vi++) + { + const double vin = vmin + vi / N16 / idiv; /* vmin .. vmax */ + mixer[i][vi] = getNormalizedValue(opampModel.solve(n, vin)); + } + } + + // 4 bit "resistor" ladders in the audio output gain + // necessitate 16 gain tables. + // From die photographs of the volume "resistor" ladders + // it follows that gain ~ vol/16 (assuming ideal op-amps + for (int n8 = 0; n8 < 16; n8++) + { + const int size = 1 << 16; + const double n = n8 / 16.0; + opampModel.reset(); + gain_vol[n8] = new unsigned short[size]; + + for (int vi = 0; vi < size; vi++) + { + const double vin = vmin + vi / N16; /* vmin .. vmax */ + gain_vol[n8][vi] = getNormalizedValue(opampModel.solve(n, vin)); + } + } + + // 4 bit "resistor" ladders in the bandpass resonance gain + // necessitate 16 gain tables. + // From die photographs of the bandpass and volume "resistor" ladders + // it follows that 1/Q ~ 2^((4 - res)/8) (assuming ideal + // op-amps and ideal "resistors"). + for (int n8 = 0; n8 < 16; n8++) + { + const int size = 1 << 16; + opampModel.reset(); + gain_res[n8] = new unsigned short[size]; + + for (int vi = 0; vi < size; vi++) + { + const double vin = vmin + vi / N16; /* vmin .. vmax */ + gain_res[n8][vi] = getNormalizedValue(opampModel.solve(resGain[n8], vin)); + } + } +} + +std::unique_ptr FilterModelConfig8580::buildIntegrator() +{ + return MAKE_UNIQUE(Integrator8580, this); +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/FilterModelConfig8580.h b/src/engine/platform/sound/c64_fp/FilterModelConfig8580.h new file mode 100644 index 00000000..ee2b4008 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/FilterModelConfig8580.h @@ -0,0 +1,68 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2020 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef FILTERMODELCONFIG8580_H +#define FILTERMODELCONFIG8580_H + +#include "FilterModelConfig.h" + +#include + +#include "sidcxx14.h" + +namespace reSIDfp +{ + +class Integrator8580; + +/** + * Calculate parameters for 8580 filter emulation. + */ +class FilterModelConfig8580 final : public FilterModelConfig +{ +private: + static std::unique_ptr instance; + // This allows access to the private constructor +#ifdef HAVE_CXX11 + friend std::unique_ptr::deleter_type; +#else + friend class std::auto_ptr; +#endif + +private: + FilterModelConfig8580(); + ~FilterModelConfig8580() DEFAULT; + +public: + static FilterModelConfig8580* getInstance(); + + /** + * Construct an integrator solver. + * + * @return the integrator + */ + std::unique_ptr buildIntegrator(); +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/Integrator6581.cpp b/src/engine/platform/sound/c64_fp/Integrator6581.cpp new file mode 100644 index 00000000..490be9b5 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Integrator6581.cpp @@ -0,0 +1,25 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2014 Leandro Nini + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define INTEGRATOR_CPP + +#include "Integrator6581.h" + +// This is needed when compiling with --disable-inline diff --git a/src/engine/platform/sound/c64_fp/Integrator6581.h b/src/engine/platform/sound/c64_fp/Integrator6581.h new file mode 100644 index 00000000..99ac3bea --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Integrator6581.h @@ -0,0 +1,285 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004, 2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef INTEGRATOR6581_H +#define INTEGRATOR6581_H + +#include "FilterModelConfig6581.h" + +#include +#include + +// uncomment to enable use of the slope factor +// in the EKV model +// actually produces worse results, needs investigation +//#define SLOPE_FACTOR + +#ifdef SLOPE_FACTOR +# include +#endif + +#include "siddefs-fp.h" + +namespace reSIDfp +{ + +/** + * Find output voltage in inverting integrator SID op-amp circuits, using a + * single fixpoint iteration step. + * + * A circuit diagram of a MOS 6581 integrator is shown below. + * + * +---C---+ + * | | + * vi --o--Rw--o-o--[A>--o-- vo + * | | vx + * +--Rs--+ + * + * From Kirchoff's current law it follows that + * + * IRw + IRs + ICr = 0 + * + * Using the formula for current through a capacitor, i = C*dv/dt, we get + * + * IRw + IRs + C*(vc - vc0)/dt = 0 + * dt/C*(IRw + IRs) + vc - vc0 = 0 + * vc = vc0 - n*(IRw(vi,vx) + IRs(vi,vx)) + * + * which may be rewritten as the following iterative fixpoint function: + * + * vc = vc0 - n*(IRw(vi,g(vc)) + IRs(vi,g(vc))) + * + * To accurately calculate the currents through Rs and Rw, we need to use + * transistor models. Rs has a gate voltage of Vdd = 12V, and can be + * assumed to always be in triode mode. For Rw, the situation is rather + * more complex, as it turns out that this transistor will operate in + * both subthreshold, triode, and saturation modes. + * + * The Shichman-Hodges transistor model routinely used in textbooks may + * be written as follows: + * + * Ids = 0 , Vgst < 0 (subthreshold mode) + * Ids = K*W/L*(2*Vgst - Vds)*Vds , Vgst >= 0, Vds < Vgst (triode mode) + * Ids = K*W/L*Vgst^2 , Vgst >= 0, Vds >= Vgst (saturation mode) + * + * where + * K = u*Cox/2 (transconductance coefficient) + * W/L = ratio between substrate width and length + * Vgst = Vg - Vs - Vt (overdrive voltage) + * + * This transistor model is also called the quadratic model. + * + * Note that the equation for the triode mode can be reformulated as + * independent terms depending on Vgs and Vgd, respectively, by the + * following substitution: + * + * Vds = Vgst - (Vgst - Vds) = Vgst - Vgdt + * + * Ids = K*W/L*(2*Vgst - Vds)*Vds + * = K*W/L*(2*Vgst - (Vgst - Vgdt)*(Vgst - Vgdt) + * = K*W/L*(Vgst + Vgdt)*(Vgst - Vgdt) + * = K*W/L*(Vgst^2 - Vgdt^2) + * + * This turns out to be a general equation which covers both the triode + * and saturation modes (where the second term is 0 in saturation mode). + * The equation is also symmetrical, i.e. it can calculate negative + * currents without any change of parameters (since the terms for drain + * and source are identical except for the sign). + * + * FIXME: Subthreshold as function of Vgs, Vgd. + * + * Ids = I0*W/L*e^(Vgst/(Ut/k)) , Vgst < 0 (subthreshold mode) + * + * where + * I0 = (2 * uCox * Ut^2) / k + * + * The remaining problem with the textbook model is that the transition + * from subthreshold the triode/saturation is not continuous. + * + * Realizing that the subthreshold and triode/saturation modes may both + * be defined by independent (and equal) terms of Vgs and Vds, + * respectively, the corresponding terms can be blended into (equal) + * continuous functions suitable for table lookup. + * + * The EKV model (Enz, Krummenacher and Vittoz) essentially performs this + * blending using an elegant mathematical formulation: + * + * Ids = Is * (if - ir) + * Is = ((2 * u*Cox * Ut^2)/k) * W/L + * if = ln^2(1 + e^((k*(Vg - Vt) - Vs)/(2*Ut)) + * ir = ln^2(1 + e^((k*(Vg - Vt) - Vd)/(2*Ut)) + * + * For our purposes, the EKV model preserves two important properties + * discussed above: + * + * - It consists of two independent terms, which can be represented by + * the same lookup table. + * - It is symmetrical, i.e. it calculates current in both directions, + * facilitating a branch-free implementation. + * + * Rw in the circuit diagram above is a VCR (voltage controlled resistor), + * as shown in the circuit diagram below. + * + * + * Vdd + * | + * Vdd _|_ + * | +---+ +---- Vw + * _|_ | + * +--+ +---o Vg + * | __|__ + * | ----- Rw + * | | | + * vi -----o------+ +-------- vo + * + * + * In order to calculalate the current through the VCR, its gate voltage + * must be determined. + * + * Assuming triode mode and applying Kirchoff's current law, we get the + * following equation for Vg: + * + * u*Cox/2*W/L*((nVddt - Vg)^2 - (nVddt - vi)^2 + (nVddt - Vg)^2 - (nVddt - Vw)^2) = 0 + * 2*(nVddt - Vg)^2 - (nVddt - vi)^2 - (nVddt - Vw)^2 = 0 + * (nVddt - Vg) = sqrt(((nVddt - vi)^2 + (nVddt - Vw)^2)/2) + * + * Vg = nVddt - sqrt(((nVddt - vi)^2 + (nVddt - Vw)^2)/2) + */ +class Integrator6581 +{ +private: + unsigned int nVddt_Vw_2; + mutable int vx; + mutable int vc; + +#ifdef SLOPE_FACTOR + // Slope factor n = 1/k + // where k is the gate coupling coefficient + // k = Cox/(Cox+Cdep) ~ 0.7 (depends on gate voltage) + mutable double n; +#endif + const unsigned short nVddt; + const unsigned short nVt; + const unsigned short nVmin; + const unsigned short nSnake; + + const FilterModelConfig6581* fmc; + +public: + Integrator6581(const FilterModelConfig6581* fmc, + double WL_snake) : + nVddt_Vw_2(0), + vx(0), + vc(0), +#ifdef SLOPE_FACTOR + n(1.4), +#endif + nVddt(fmc->getNormalizedValue(fmc->getVddt())), + nVt(fmc->getNormalizedValue(fmc->getVth())), + nVmin(fmc->getNVmin()), + nSnake(fmc->getNormalizedCurrentFactor(WL_snake)), + fmc(fmc) {} + + void setVw(unsigned short Vw) { nVddt_Vw_2 = ((nVddt - Vw) * (nVddt - Vw)) >> 1; } + + int solve(int vi) const; +}; + +} // namespace reSIDfp + +#if RESID_INLINING || defined(INTEGRATOR_CPP) + +namespace reSIDfp +{ + +RESID_INLINE +int Integrator6581::solve(int vi) const +{ + // Make sure Vgst>0 so we're not in subthreshold mode + assert(vx < nVddt); + + // Check that transistor is actually in triode mode + // Vds < Vgs - Vth + assert(vi < nVddt); + + // "Snake" voltages for triode mode calculation. + const unsigned int Vgst = nVddt - vx; + const unsigned int Vgdt = nVddt - vi; + + const unsigned int Vgst_2 = Vgst * Vgst; + const unsigned int Vgdt_2 = Vgdt * Vgdt; + + // "Snake" current, scaled by (1/m)*2^13*m*2^16*m*2^16*2^-15 = m*2^30 + const int n_I_snake = nSnake * (static_cast(Vgst_2 - Vgdt_2) >> 15); + + // VCR gate voltage. // Scaled by m*2^16 + // Vg = Vddt - sqrt(((Vddt - Vw)^2 + Vgdt^2)/2) + const int nVg = static_cast(fmc->getVcr_nVg((nVddt_Vw_2 + (Vgdt_2 >> 1)) >> 16)); +#ifdef SLOPE_FACTOR + const double nVp = static_cast(nVg - nVt) / n; // Pinch-off voltage + const int kVg = static_cast(nVp + 0.5) - nVmin; +#else + const int kVg = (nVg - nVt) - nVmin; +#endif + + // VCR voltages for EKV model table lookup. + const int kVgt_Vs = (vx < kVg) ? kVg - vx : 0; + assert(kVgt_Vs < (1 << 16)); + const int kVgt_Vd = (vi < kVg) ? kVg - vi : 0; + assert(kVgt_Vd < (1 << 16)); + + // VCR current, scaled by m*2^15*2^15 = m*2^30 + const unsigned int If = static_cast(fmc->getVcr_n_Ids_term(kVgt_Vs)) << 15; + const unsigned int Ir = static_cast(fmc->getVcr_n_Ids_term(kVgt_Vd)) << 15; +#ifdef SLOPE_FACTOR + const double iVcr = static_cast(If - Ir); + const int n_I_vcr = static_cast((iVcr * n) + 0.5); +#else + const int n_I_vcr = If - Ir; +#endif + +#ifdef SLOPE_FACTOR + // estimate new slope factor based on gate voltage + const double gamma = 1.0; // body effect factor + const double phi = 0.8; // bulk Fermi potential + const double Vp = nVp / fmc->getN16(); + n = 1. + (gamma / (2. * sqrt(Vp + phi + 4. * fmc->getUt()))); + assert((n > 1.2) && (n < 1.8)); +#endif + + // Change in capacitor charge. + vc += n_I_snake + n_I_vcr; + + // vx = g(vc) + const int tmp = (vc >> 15) + (1 << 15); + assert(tmp < (1 << 16)); + vx = fmc->getOpampRev(tmp); + + // Return vo. + return vx - (vc >> 14); +} + +} // namespace reSIDfp + +#endif + +#endif diff --git a/src/engine/platform/sound/c64_fp/Integrator8580.cpp b/src/engine/platform/sound/c64_fp/Integrator8580.cpp new file mode 100644 index 00000000..6fba9521 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Integrator8580.cpp @@ -0,0 +1,25 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2014-2016 Leandro Nini + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define INTEGRATOR8580_CPP + +#include "Integrator8580.h" + +// This is needed when compiling with --disable-inline diff --git a/src/engine/platform/sound/c64_fp/Integrator8580.h b/src/engine/platform/sound/c64_fp/Integrator8580.h new file mode 100644 index 00000000..7137e940 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Integrator8580.h @@ -0,0 +1,142 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004, 2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef INTEGRATOR8580_H +#define INTEGRATOR8580_H + +#include "FilterModelConfig8580.h" + +#include +#include + +#include "siddefs-fp.h" + +namespace reSIDfp +{ + +/** + * 8580 integrator + * + * +---C---+ + * | | + * vi -----Rfc---o--[A>--o-- vo + * vx + * + * IRfc + ICr = 0 + * IRfc + C*(vc - vc0)/dt = 0 + * dt/C*(IRfc) + vc - vc0 = 0 + * vc = vc0 - n*(IRfc(vi,vx)) + * vc = vc0 - n*(IRfc(vi,g(vc))) + * + * IRfc = K*W/L*(Vgst^2 - Vgdt^2) = n*((Vddt - vx)^2 - (Vddt - vi)^2) + * + * Rfc gate voltage is generated by an OP Amp and depends on chip temperature. + */ +class Integrator8580 +{ +private: + mutable int vx; + mutable int vc; + + unsigned short nVgt; + unsigned short n_dac; + + const FilterModelConfig8580* fmc; + +public: + Integrator8580(const FilterModelConfig8580* fmc) : + vx(0), + vc(0), + fmc(fmc) + { + setV(1.5); + } + + /** + * Set Filter Cutoff resistor ratio. + */ + void setFc(double wl) + { + // Normalized current factor, 1 cycle at 1MHz. + // Fit in 5 bits. + n_dac = fmc->getNormalizedCurrentFactor(wl); + } + + /** + * Set FC gate voltage multiplier. + */ + void setV(double v) + { + // Gate voltage is controlled by the switched capacitor voltage divider + // Ua = Ue * v = 4.76v 1 1.0 && v < 2.0); + const double Vg = fmc->getVoiceDCVoltage() * v; + const double Vgt = Vg - fmc->getVth(); + + // Vg - Vth, normalized so that translated values can be subtracted: + // Vgt - x = (Vgt - t) - (x - t) + nVgt = fmc->getNormalizedValue(Vgt); + } + + int solve(int vi) const; +}; + +} // namespace reSIDfp + +#if RESID_INLINING || defined(INTEGRATOR8580_CPP) + +namespace reSIDfp +{ + +RESID_INLINE +int Integrator8580::solve(int vi) const +{ + // Make sure we're not in subthreshold mode + assert(vx < nVgt); + + // DAC voltages + const unsigned int Vgst = nVgt - vx; + const unsigned int Vgdt = (vi < nVgt) ? nVgt - vi : 0; // triode/saturation mode + + const unsigned int Vgst_2 = Vgst * Vgst; + const unsigned int Vgdt_2 = Vgdt * Vgdt; + + // DAC current, scaled by (1/m)*2^13*m*2^16*m*2^16*2^-15 = m*2^30 + const int n_I_dac = n_dac * (static_cast(Vgst_2 - Vgdt_2) >> 15); + + // Change in capacitor charge. + vc += n_I_dac; + + // vx = g(vc) + const int tmp = (vc >> 15) + (1 << 15); + assert(tmp < (1 << 16)); + vx = fmc->getOpampRev(tmp); + + // Return vo. + return vx - (vc >> 14); +} + +} // namespace reSIDfp + +#endif + +#endif diff --git a/src/engine/platform/sound/c64_fp/OpAmp.cpp b/src/engine/platform/sound/c64_fp/OpAmp.cpp new file mode 100644 index 00000000..b26b2efc --- /dev/null +++ b/src/engine/platform/sound/c64_fp/OpAmp.cpp @@ -0,0 +1,84 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2015 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "OpAmp.h" + +#include + +#include "siddefs-fp.h" + +namespace reSIDfp +{ + +const double EPSILON = 1e-8; + +double OpAmp::solve(double n, double vi) const +{ + // Start off with an estimate of x and a root bracket [ak, bk]. + // f is decreasing, so that f(ak) > 0 and f(bk) < 0. + double ak = vmin; + double bk = vmax; + + const double a = n + 1.; + const double b = Vddt; + const double b_vi = (b > vi) ? (b - vi) : 0.; + const double c = n * (b_vi * b_vi); + + for (;;) + { + const double xk = x; + + // Calculate f and df. + + Spline::Point out = opamp->evaluate(x); + const double vo = out.x; + const double dvo = out.y; + + const double b_vx = (b > x) ? b - x : 0.; + const double b_vo = (b > vo) ? b - vo : 0.; + + // f = a*(b - vx)^2 - c - (b - vo)^2 + const double f = a * (b_vx * b_vx) - c - (b_vo * b_vo); + + // df = 2*((b - vo)*dvo - a*(b - vx)) + const double df = 2. * (b_vo * dvo - a * b_vx); + + // Newton-Raphson step: xk1 = xk - f(xk)/f'(xk) + x -= f / df; + + if (unlikely(fabs(x - xk) < EPSILON)) + { + out = opamp->evaluate(x); + return out.x; + } + + // Narrow down root bracket. + (f < 0. ? bk : ak) = xk; + + if (unlikely(x <= ak) || unlikely(x >= bk)) + { + // Bisection step (ala Dekker's method). + x = (ak + bk) * 0.5; + } + } +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/OpAmp.h b/src/engine/platform/sound/c64_fp/OpAmp.h new file mode 100644 index 00000000..9d2c8f16 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/OpAmp.h @@ -0,0 +1,113 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2015 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef OPAMP_H +#define OPAMP_H + +#include +#include + +#include "Spline.h" + +#include "sidcxx11.h" + +namespace reSIDfp +{ + +/** + * Find output voltage in inverting gain and inverting summer SID op-amp + * circuits, using a combination of Newton-Raphson and bisection. + * + * +---R2--+ + * | | + * vi ---R1--o--[A>--o-- vo + * vx + * + * From Kirchoff's current law it follows that + * + * IR1f + IR2r = 0 + * + * Substituting the triode mode transistor model K*W/L*(Vgst^2 - Vgdt^2) + * for the currents, we get: + * + * n*((Vddt - vx)^2 - (Vddt - vi)^2) + (Vddt - vx)^2 - (Vddt - vo)^2 = 0 + * + * Our root function f can thus be written as: + * + * f = (n + 1)*(Vddt - vx)^2 - n*(Vddt - vi)^2 - (Vddt - vo)^2 = 0 + * + * Using substitution constants + * + * a = n + 1 + * b = Vddt + * c = n*(Vddt - vi)^2 + * + * the equations for the root function and its derivative can be written as: + * + * f = a*(b - vx)^2 - c - (b - vo)^2 + * df = 2*((b - vo)*dvo - a*(b - vx)) + */ +class OpAmp +{ +private: + /// Current root position (cached as guess to speed up next iteration) + mutable double x; + + const double Vddt; + const double vmin; + const double vmax; + + std::unique_ptr const opamp; + +public: + /** + * Opamp input -> output voltage conversion + * + * @param opamp opamp mapping table as pairs of points (in -> out) + * @param opamplength length of the opamp array + * @param kVddt transistor dt parameter (in volts) + */ + OpAmp(const std::vector &opamp, double Vddt) : + x(0.), + Vddt(Vddt), + vmin(opamp.front().x), + vmax(opamp.back().x), + opamp(new Spline(opamp)) {} + + void reset() const + { + x = vmin; + } + + /** + * Solve the opamp equation for input vi in loading context n + * + * @param n the ratio of input/output loading + * @param vi input + * @return vo + */ + double solve(double n, double vi) const; +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/Potentiometer.h b/src/engine/platform/sound/c64_fp/Potentiometer.h new file mode 100644 index 00000000..8b63df13 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Potentiometer.h @@ -0,0 +1,50 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2013 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright (C) 2004 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef POTENTIOMETER_H +#define POTENTIOMETER_H + +namespace reSIDfp +{ + +/** + * Potentiometer representation. + * + * This class will probably never be implemented in any real way. + * + * @author Ken Händel + * @author Dag Lem + */ +class Potentiometer +{ +public: + /** + * Read paddle value. Not modeled. + * + * @return paddle value (always 0xff) + */ + unsigned char readPOT() const { return 0xff; } +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/README b/src/engine/platform/sound/c64_fp/README new file mode 100644 index 00000000..45d4bfb9 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/README @@ -0,0 +1,20 @@ +reSIDfp is a fork of Dag Lem's reSID 0.16, a reverse engineered software emulation +of the MOS6581/8580 SID (Sound Interface Device). + +The project was started by Antti S. Lankila in order to improve SID emulation +with special focus on the 6581 filter. +The codebase has been later on ported to java by Ken Händel within the jsidplay2 project +and has seen further work by Antti Lankila. +It was then ported back to c++ and integrated with improvements from reSID 1.0 by Leandro Nini. + + +Main differences from reSID: + +* combined waveforms are emulated by a parametrized model based on samplings from Kevtris; +* envelope generator is implemented like in the real machine with a shift register; +* high quality resampling is done in two steps to allow computational savings using lower order filters; +* part of the calculations are done with floats instead of fixed point; +* interpolation is accomplished with Fritsch-Carlson method to preserve monotonicity. + + +reSIDfp is free software. See the file COPYING for copying permission. diff --git a/src/engine/platform/sound/c64_fp/SID.cpp b/src/engine/platform/sound/c64_fp/SID.cpp new file mode 100644 index 00000000..a996d223 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/SID.cpp @@ -0,0 +1,504 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2016 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define SID_CPP + +#include "SID.h" + +#include + +#include "array.h" +#include "Dac.h" +#include "Filter6581.h" +#include "Filter8580.h" +#include "Potentiometer.h" +#include "WaveformCalculator.h" +#include "resample/TwoPassSincResampler.h" +#include "resample/ZeroOrderResampler.h" + +namespace reSIDfp +{ + +const unsigned int ENV_DAC_BITS = 8; +const unsigned int OSC_DAC_BITS = 12; + +/** + * The waveform D/A converter introduces a DC offset in the signal + * to the envelope multiplying D/A converter. The "zero" level of + * the waveform D/A converter can be found as follows: + * + * Measure the "zero" voltage of voice 3 on the SID audio output + * pin, routing only voice 3 to the mixer ($d417 = $0b, $d418 = + * $0f, all other registers zeroed). + * + * Then set the sustain level for voice 3 to maximum and search for + * the waveform output value yielding the same voltage as found + * above. This is done by trying out different waveform output + * values until the correct value is found, e.g. with the following + * program: + * + * lda #$08 + * sta $d412 + * lda #$0b + * sta $d417 + * lda #$0f + * sta $d418 + * lda #$f0 + * sta $d414 + * lda #$21 + * sta $d412 + * lda #$01 + * sta $d40e + * + * ldx #$00 + * lda #$38 ; Tweak this to find the "zero" level + *l cmp $d41b + * bne l + * stx $d40e ; Stop frequency counter - freeze waveform output + * brk + * + * The waveform output range is 0x000 to 0xfff, so the "zero" + * level should ideally have been 0x800. In the measured chip, the + * waveform output "zero" level was found to be 0x380 (i.e. $d41b + * = 0x38) at an audio output voltage of 5.94V. + * + * With knowledge of the mixer op-amp characteristics, further estimates + * of waveform voltages can be obtained by sampling the EXT IN pin. + * From EXT IN samples, the corresponding waveform output can be found by + * using the model for the mixer. + * + * Such measurements have been done on a chip marked MOS 6581R4AR + * 0687 14, and the following results have been obtained: + * * The full range of one voice is approximately 1.5V. + * * The "zero" level rides at approximately 5.0V. + * + * + * zero-x did the measuring on the 8580 (https://sourceforge.net/p/vice-emu/bugs/1036/#c5b3): + * When it sits on basic from powerup it's at 4.72 + * Run 1.prg and check the output pin level. + * Then run 2.prg andadjust it until the output level is the same... + * 0x94-0xA8 gives me the same 4.72 1.prg shows. + * On another 8580 it's 0x90-0x9C + * Third chip 0x94-0xA8 + * Fourth chip 0x90-0xA4 + * On the 8580 that plays digis the output is 4.66 and 0x93 is the only value to reach that. + * To me that seems as regular 8580s have somewhat wide 0-level range, + * whereas that digi-compatible 8580 has it very narrow. + * On my 6581R4AR has 0x3A as the only value giving the same output level as 1.prg + */ +//@{ +unsigned int constexpr OFFSET_6581 = 0x380; +unsigned int constexpr OFFSET_8580 = 0x9c0; +//@} + +/** + * Bus value stays alive for some time after each operation. + * Values differs between chip models, the timings used here + * are taken from VICE [1]. + * See also the discussion "How do I reliably detect 6581/8580 sid?" on CSDb [2]. + * + * Results from real C64 (testprogs/SID/bitfade/delayfrq0.prg): + * + * (new SID) (250469/8580R5) (250469/8580R5) + * delayfrq0 ~7a000 ~108000 + * + * (old SID) (250407/6581) + * delayfrq0 ~01d00 + * + * [1]: http://sourceforge.net/p/vice-emu/patches/99/ + * [2]: http://noname.c64.org/csdb/forums/?roomid=11&topicid=29025&showallposts=1 + */ +//@{ +int constexpr BUS_TTL_6581 = 0x01d00; +int constexpr BUS_TTL_8580 = 0xa2000; +//@} + +SID::SID() : + filter6581(new Filter6581()), + filter8580(new Filter8580()), + externalFilter(new ExternalFilter()), + resampler(nullptr), + potX(new Potentiometer()), + potY(new Potentiometer()) +{ + voice[0].reset(new Voice()); + voice[1].reset(new Voice()); + voice[2].reset(new Voice()); + + muted[0] = muted[1] = muted[2] = false; + + reset(); + setChipModel(MOS8580); +} + +SID::~SID() +{ + // Needed to delete auto_ptr with complete type +} + +void SID::setFilter6581Curve(double filterCurve) +{ + filter6581->setFilterCurve(filterCurve); +} + +void SID::setFilter8580Curve(double filterCurve) +{ + filter8580->setFilterCurve(filterCurve); +} + +void SID::enableFilter(bool enable) +{ + filter6581->enable(enable); + filter8580->enable(enable); +} + +void SID::voiceSync(bool sync) +{ + if (sync) + { + // Synchronize the 3 waveform generators. + for (int i = 0; i < 3; i++) + { + voice[i]->wave()->synchronize(voice[(i + 1) % 3]->wave(), voice[(i + 2) % 3]->wave()); + } + } + + // Calculate the time to next voice sync + nextVoiceSync = std::numeric_limits::max(); + + for (int i = 0; i < 3; i++) + { + WaveformGenerator* const wave = voice[i]->wave(); + const unsigned int freq = wave->readFreq(); + + if (wave->readTest() || freq == 0 || !voice[(i + 1) % 3]->wave()->readSync()) + { + continue; + } + + const unsigned int accumulator = wave->readAccumulator(); + const unsigned int thisVoiceSync = ((0x7fffff - accumulator) & 0xffffff) / freq + 1; + + if (thisVoiceSync < nextVoiceSync) + { + nextVoiceSync = thisVoiceSync; + } + } +} + +void SID::setChipModel(ChipModel model) +{ + switch (model) + { + case MOS6581: + filter = filter6581.get(); + modelTTL = BUS_TTL_6581; + break; + + case MOS8580: + filter = filter8580.get(); + modelTTL = BUS_TTL_8580; + break; + + default: + throw SIDError("Unknown chip type"); + } + + this->model = model; + + // calculate waveform-related tables + matrix_t* tables = WaveformCalculator::getInstance()->buildTable(model); + + // calculate envelope DAC table + { + Dac dacBuilder(ENV_DAC_BITS); + dacBuilder.kinkedDac(model); + + for (unsigned int i = 0; i < (1 << ENV_DAC_BITS); i++) + { + envDAC[i] = static_cast(dacBuilder.getOutput(i)); + } + } + + // calculate oscillator DAC table + const bool is6581 = model == MOS6581; + + { + Dac dacBuilder(OSC_DAC_BITS); + dacBuilder.kinkedDac(model); + + const double offset = dacBuilder.getOutput(is6581 ? OFFSET_6581 : OFFSET_8580); + + for (unsigned int i = 0; i < (1 << OSC_DAC_BITS); i++) + { + const double dacValue = dacBuilder.getOutput(i); + oscDAC[i] = static_cast(dacValue - offset); + } + } + + // set voice tables + for (int i = 0; i < 3; i++) + { + voice[i]->setEnvDAC(envDAC); + voice[i]->setWavDAC(oscDAC); + voice[i]->wave()->setModel(is6581); + voice[i]->wave()->setWaveformModels(tables); + } +} + +void SID::reset() +{ + for (int i = 0; i < 3; i++) + { + voice[i]->reset(); + } + + filter6581->reset(); + filter8580->reset(); + externalFilter->reset(); + + if (resampler.get()) + { + resampler->reset(); + } + + busValue = 0; + busValueTtl = 0; + voiceSync(false); +} + +void SID::input(int value) +{ + filter6581->input(value); + filter8580->input(value); +} + +unsigned char SID::read(int offset) +{ + switch (offset) + { + case 0x19: // X value of paddle + busValue = potX->readPOT(); + busValueTtl = modelTTL; + break; + + case 0x1a: // Y value of paddle + busValue = potY->readPOT(); + busValueTtl = modelTTL; + break; + + case 0x1b: // Voice #3 waveform output + busValue = voice[2]->wave()->readOSC(); + busValueTtl = modelTTL; + break; + + case 0x1c: // Voice #3 ADSR output + busValue = voice[2]->envelope()->readENV(); + busValueTtl = modelTTL; + break; + + default: + // Reading from a write-only or non-existing register + // makes the bus discharge faster. + // Emulate this by halving the residual TTL. + busValueTtl /= 2; + break; + } + + return busValue; +} + +void SID::write(int offset, unsigned char value) +{ + busValue = value; + busValueTtl = modelTTL; + + switch (offset) + { + case 0x00: // Voice #1 frequency (Low-byte) + voice[0]->wave()->writeFREQ_LO(value); + break; + + case 0x01: // Voice #1 frequency (High-byte) + voice[0]->wave()->writeFREQ_HI(value); + break; + + case 0x02: // Voice #1 pulse width (Low-byte) + voice[0]->wave()->writePW_LO(value); + break; + + case 0x03: // Voice #1 pulse width (bits #8-#15) + voice[0]->wave()->writePW_HI(value); + break; + + case 0x04: // Voice #1 control register + voice[0]->writeCONTROL_REG(muted[0] ? 0 : value); + break; + + case 0x05: // Voice #1 Attack and Decay length + voice[0]->envelope()->writeATTACK_DECAY(value); + break; + + case 0x06: // Voice #1 Sustain volume and Release length + voice[0]->envelope()->writeSUSTAIN_RELEASE(value); + break; + + case 0x07: // Voice #2 frequency (Low-byte) + voice[1]->wave()->writeFREQ_LO(value); + break; + + case 0x08: // Voice #2 frequency (High-byte) + voice[1]->wave()->writeFREQ_HI(value); + break; + + case 0x09: // Voice #2 pulse width (Low-byte) + voice[1]->wave()->writePW_LO(value); + break; + + case 0x0a: // Voice #2 pulse width (bits #8-#15) + voice[1]->wave()->writePW_HI(value); + break; + + case 0x0b: // Voice #2 control register + voice[1]->writeCONTROL_REG(muted[1] ? 0 : value); + break; + + case 0x0c: // Voice #2 Attack and Decay length + voice[1]->envelope()->writeATTACK_DECAY(value); + break; + + case 0x0d: // Voice #2 Sustain volume and Release length + voice[1]->envelope()->writeSUSTAIN_RELEASE(value); + break; + + case 0x0e: // Voice #3 frequency (Low-byte) + voice[2]->wave()->writeFREQ_LO(value); + break; + + case 0x0f: // Voice #3 frequency (High-byte) + voice[2]->wave()->writeFREQ_HI(value); + break; + + case 0x10: // Voice #3 pulse width (Low-byte) + voice[2]->wave()->writePW_LO(value); + break; + + case 0x11: // Voice #3 pulse width (bits #8-#15) + voice[2]->wave()->writePW_HI(value); + break; + + case 0x12: // Voice #3 control register + voice[2]->writeCONTROL_REG(muted[2] ? 0 : value); + break; + + case 0x13: // Voice #3 Attack and Decay length + voice[2]->envelope()->writeATTACK_DECAY(value); + break; + + case 0x14: // Voice #3 Sustain volume and Release length + voice[2]->envelope()->writeSUSTAIN_RELEASE(value); + break; + + case 0x15: // Filter cut off frequency (bits #0-#2) + filter6581->writeFC_LO(value); + filter8580->writeFC_LO(value); + break; + + case 0x16: // Filter cut off frequency (bits #3-#10) + filter6581->writeFC_HI(value); + filter8580->writeFC_HI(value); + break; + + case 0x17: // Filter control + filter6581->writeRES_FILT(value); + filter8580->writeRES_FILT(value); + break; + + case 0x18: // Volume and filter modes + filter6581->writeMODE_VOL(value); + filter8580->writeMODE_VOL(value); + break; + + default: + break; + } + + // Update voicesync just in case. + voiceSync(false); +} + +void SID::setSamplingParameters(double clockFrequency, SamplingMethod method, double samplingFrequency, double highestAccurateFrequency) +{ + externalFilter->setClockFrequency(clockFrequency); + + switch (method) + { + case DECIMATE: + resampler.reset(new ZeroOrderResampler(clockFrequency, samplingFrequency)); + break; + + case RESAMPLE: + resampler.reset(TwoPassSincResampler::create(clockFrequency, samplingFrequency, highestAccurateFrequency)); + break; + + default: + throw SIDError("Unknown sampling method"); + } +} + +void SID::clockSilent(unsigned int cycles) +{ + ageBusValue(cycles); + + while (cycles != 0) + { + int delta_t = std::min(nextVoiceSync, cycles); + + if (delta_t > 0) + { + for (int i = 0; i < delta_t; i++) + { + // clock waveform generators (can affect OSC3) + voice[0]->wave()->clock(); + voice[1]->wave()->clock(); + voice[2]->wave()->clock(); + + voice[0]->wave()->output(voice[2]->wave()); + voice[1]->wave()->output(voice[0]->wave()); + voice[2]->wave()->output(voice[1]->wave()); + + // clock ENV3 only + voice[2]->envelope()->clock(); + } + + cycles -= delta_t; + nextVoiceSync -= delta_t; + } + + if (nextVoiceSync == 0) + { + voiceSync(true); + } + } +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/SID.h b/src/engine/platform/sound/c64_fp/SID.h new file mode 100644 index 00000000..05ad83c3 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/SID.h @@ -0,0 +1,372 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2016 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef SIDFP_H +#define SIDFP_H + +#include + +#include "siddefs-fp.h" + +#include "sidcxx11.h" + +namespace reSIDfp +{ + +class Filter; +class Filter6581; +class Filter8580; +class ExternalFilter; +class Potentiometer; +class Voice; +class Resampler; + +/** + * SID error exception. + */ +class SIDError +{ +private: + const char* message; + +public: + SIDError(const char* msg) : + message(msg) {} + const char* getMessage() const { return message; } +}; + +/** + * MOS6581/MOS8580 emulation. + */ +class SID +{ +private: + /// Currently active filter + Filter* filter; + + /// Filter used, if model is set to 6581 + std::unique_ptr const filter6581; + + /// Filter used, if model is set to 8580 + std::unique_ptr const filter8580; + + /** + * External filter that provides high-pass and low-pass filtering + * to adjust sound tone slightly. + */ + std::unique_ptr const externalFilter; + + /// Resampler used by audio generation code. + std::unique_ptr resampler; + + /// Paddle X register support + std::unique_ptr const potX; + + /// Paddle Y register support + std::unique_ptr const potY; + + /// SID voices + std::unique_ptr voice[3]; + + /// Time to live for the last written value + int busValueTtl; + + /// Current chip model's bus value TTL + int modelTTL; + + /// Time until #voiceSync must be run. + unsigned int nextVoiceSync; + + /// Currently active chip model. + ChipModel model; + + /// Last written value + unsigned char busValue; + + /// Flags for muted channels + bool muted[3]; + + /** + * Emulated nonlinearity of the envelope DAC. + * + * @See Dac + */ + float envDAC[256]; + + /** + * Emulated nonlinearity of the oscillator DAC. + * + * @See Dac + */ + float oscDAC[4096]; + +private: + /** + * Age the bus value and zero it if it's TTL has expired. + * + * @param n the number of cycles + */ + void ageBusValue(unsigned int n); + + /** + * Get output sample. + * + * @return the output sample + */ + int output() const; + + /** + * Calculate the numebr of cycles according to current parameters + * that it takes to reach sync. + * + * @param sync whether to do the actual voice synchronization + */ + void voiceSync(bool sync); + +public: + SID(); + ~SID(); + + /** + * Set chip model. + * + * @param model chip model to use + * @throw SIDError + */ + void setChipModel(ChipModel model); + + /** + * Get currently emulated chip model. + */ + ChipModel getChipModel() const { return model; } + + /** + * SID reset. + */ + void reset(); + + /** + * 16-bit input (EXT IN). Write 16-bit sample to audio input. NB! The caller + * is responsible for keeping the value within 16 bits. Note that to mix in + * an external audio signal, the signal should be resampled to 1MHz first to + * avoid sampling noise. + * + * @param value input level to set + */ + void input(int value); + + /** + * Read registers. + * + * Reading a write only register returns the last char written to any SID register. + * The individual bits in this value start to fade down towards zero after a few cycles. + * All bits reach zero within approximately $2000 - $4000 cycles. + * It has been claimed that this fading happens in an orderly fashion, + * however sampling of write only registers reveals that this is not the case. + * NOTE: This is not correctly modeled. + * The actual use of write only registers has largely been made + * in the belief that all SID registers are readable. + * To support this belief the read would have to be done immediately + * after a write to the same register (remember that an intermediate write + * to another register would yield that value instead). + * With this in mind we return the last value written to any SID register + * for $2000 cycles without modeling the bit fading. + * + * @param offset SID register to read + * @return value read from chip + */ + unsigned char read(int offset); + + /** + * Write registers. + * + * @param offset chip register to write + * @param value value to write + */ + void write(int offset, unsigned char value); + + /** + * SID voice muting. + * + * @param channel channel to modify + * @param enable is muted? + */ + void mute(int channel, bool enable) { muted[channel] = enable; } + + /** + * Setting of SID sampling parameters. + * + * Use a clock freqency of 985248Hz for PAL C64, 1022730Hz for NTSC C64. + * The default end of passband frequency is pass_freq = 0.9*sample_freq/2 + * for sample frequencies up to ~ 44.1kHz, and 20kHz for higher sample frequencies. + * + * For resampling, the ratio between the clock frequency and the sample frequency + * is limited as follows: 125*clock_freq/sample_freq < 16384 + * E.g. provided a clock frequency of ~ 1MHz, the sample frequency can not be set + * lower than ~ 8kHz. A lower sample frequency would make the resampling code + * overfill its 16k sample ring buffer. + * + * The end of passband frequency is also limited: pass_freq <= 0.9*sample_freq/2 + * + * E.g. for a 44.1kHz sampling rate the end of passband frequency + * is limited to slightly below 20kHz. + * This constraint ensures that the FIR table is not overfilled. + * + * @param clockFrequency System clock frequency at Hz + * @param method sampling method to use + * @param samplingFrequency Desired output sampling rate + * @param highestAccurateFrequency + * @throw SIDError + */ + void setSamplingParameters(double clockFrequency, SamplingMethod method, double samplingFrequency, double highestAccurateFrequency); + + /** + * Clock SID forward using chosen output sampling algorithm. + * + * @param cycles c64 clocks to clock + * @param buf audio output buffer + * @return number of samples produced + */ + int clock(unsigned int cycles, short* buf); + + /** + * Clock SID forward with no audio production. + * + * _Warning_: + * You can't mix this method of clocking with the audio-producing + * clock() because components that don't affect OSC3/ENV3 are not + * emulated. + * + * @param cycles c64 clocks to clock. + */ + void clockSilent(unsigned int cycles); + + /** + * Set filter curve parameter for 6581 model. + * + * @see Filter6581::setFilterCurve(double) + */ + void setFilter6581Curve(double filterCurve); + + /** + * Set filter curve parameter for 8580 model. + * + * @see Filter8580::setFilterCurve(double) + */ + void setFilter8580Curve(double filterCurve); + + /** + * Enable filter emulation. + * + * @param enable false to turn off filter emulation + */ + void enableFilter(bool enable); +}; + +} // namespace reSIDfp + +#if RESID_INLINING || defined(SID_CPP) + +#include + +#include "Filter.h" +#include "ExternalFilter.h" +#include "Voice.h" +#include "resample/Resampler.h" + +namespace reSIDfp +{ + +RESID_INLINE +void SID::ageBusValue(unsigned int n) +{ + if (likely(busValueTtl != 0)) + { + busValueTtl -= n; + + if (unlikely(busValueTtl <= 0)) + { + busValue = 0; + busValueTtl = 0; + } + } +} + +RESID_INLINE +int SID::output() const +{ + const int v1 = voice[0]->output(voice[2]->wave()); + const int v2 = voice[1]->output(voice[0]->wave()); + const int v3 = voice[2]->output(voice[1]->wave()); + + return externalFilter->clock(filter->clock(v1, v2, v3)); +} + + +RESID_INLINE +int SID::clock(unsigned int cycles, short* buf) +{ + ageBusValue(cycles); + int s = 0; + + while (cycles != 0) + { + unsigned int delta_t = std::min(nextVoiceSync, cycles); + + if (likely(delta_t > 0)) + { + for (unsigned int i = 0; i < delta_t; i++) + { + // clock waveform generators + voice[0]->wave()->clock(); + voice[1]->wave()->clock(); + voice[2]->wave()->clock(); + + // clock envelope generators + voice[0]->envelope()->clock(); + voice[1]->envelope()->clock(); + voice[2]->envelope()->clock(); + + if (unlikely(resampler->input(output()))) + { + buf[s++] = resampler->getOutput(); + } + } + + cycles -= delta_t; + nextVoiceSync -= delta_t; + } + + if (unlikely(nextVoiceSync == 0)) + { + voiceSync(true); + } + } + + return s; +} + +} // namespace reSIDfp + +#endif + +#endif diff --git a/src/engine/platform/sound/c64_fp/Spline.cpp b/src/engine/platform/sound/c64_fp/Spline.cpp new file mode 100644 index 00000000..50d55fef --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Spline.cpp @@ -0,0 +1,119 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2015 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "Spline.h" + +#include +#include + +namespace reSIDfp +{ + +Spline::Spline(const std::vector &input) : + params(input.size()), + c(¶ms[0]) +{ + assert(input.size() > 2); + + const size_t coeffLength = input.size() - 1; + + std::vector dxs(coeffLength); + std::vector ms(coeffLength); + + // Get consecutive differences and slopes + for (size_t i = 0; i < coeffLength; i++) + { + assert(input[i].x < input[i + 1].x); + + const double dx = input[i + 1].x - input[i].x; + const double dy = input[i + 1].y - input[i].y; + dxs[i] = dx; + ms[i] = dy/dx; + } + + // Get degree-1 coefficients + params[0].c = ms[0]; + for (size_t i = 1; i < coeffLength; i++) + { + const double m = ms[i - 1]; + const double mNext = ms[i]; + if (m * mNext <= 0) + { + params[i].c = 0.0; + } + else + { + const double dx = dxs[i - 1]; + const double dxNext = dxs[i]; + const double common = dx + dxNext; + params[i].c = 3.0 * common / ((common + dxNext) / m + (common + dx) / mNext); + } + } + params[coeffLength].c = ms[coeffLength - 1]; + + // Get degree-2 and degree-3 coefficients + for (size_t i = 0; i < coeffLength; i++) + { + params[i].x1 = input[i].x; + params[i].x2 = input[i + 1].x; + params[i].d = input[i].y; + + const double c1 = params[i].c; + const double m = ms[i]; + const double invDx = 1.0 / dxs[i]; + const double common = c1 + params[i + 1].c - m - m; + params[i].b = (m - c1 - common) * invDx; + params[i].a = common * invDx * invDx; + } + + // Fix the upper range, because we interpolate outside original bounds if necessary. + params[coeffLength - 1].x2 = std::numeric_limits::max(); +} + +Spline::Point Spline::evaluate(double x) const +{ + if ((x < c->x1) || (x > c->x2)) + { + for (size_t i = 0; i < params.size(); i++) + { + if (x <= params[i].x2) + { + c = ¶ms[i]; + break; + } + } + } + + // Interpolate + const double diff = x - c->x1; + + Point out; + + // y = a*x^3 + b*x^2 + c*x + d + out.x = ((c->a * diff + c->b) * diff + c->c) * diff + c->d; + + // dy = 3*a*x^2 + 2*b*x + c + out.y = (3.0 * c->a * diff + 2.0 * c->b) * diff + c->c; + + return out; +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/Spline.h b/src/engine/platform/sound/c64_fp/Spline.h new file mode 100644 index 00000000..6cc2b1ed --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Spline.h @@ -0,0 +1,78 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2015 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef SPLINE_H +#define SPLINE_H + +#include +#include + +namespace reSIDfp +{ + +/** + * Fritsch-Carlson monotone cubic spline interpolation. + * + * Based on the implementation from the [Monotone cubic interpolation] wikipedia page. + * + * [Monotone cubic interpolation]: https://en.wikipedia.org/wiki/Monotone_cubic_interpolation + */ +class Spline +{ +public: + typedef struct + { + double x; + double y; + } Point; + +private: + typedef struct + { + double x1; + double x2; + double a; + double b; + double c; + double d; + } Param; + + typedef std::vector ParamVector; + +private: + /// Interpolation parameters + ParamVector params; + + /// Last used parameters, cached for speed up + mutable ParamVector::const_pointer c; + +public: + Spline(const std::vector &input); + + /** + * Evaluate y and its derivative at given point x. + */ + Point evaluate(double x) const; +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/Voice.h b/src/engine/platform/sound/c64_fp/Voice.h new file mode 100644 index 00000000..fc7ed41b --- /dev/null +++ b/src/engine/platform/sound/c64_fp/Voice.h @@ -0,0 +1,130 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef VOICE_H +#define VOICE_H + +#include + +#include "siddefs-fp.h" +#include "WaveformGenerator.h" +#include "EnvelopeGenerator.h" + +#include "sidcxx11.h" + +namespace reSIDfp +{ + +/** + * Representation of SID voice block. + */ +class Voice +{ +private: + std::unique_ptr const waveformGenerator; + + std::unique_ptr const envelopeGenerator; + + /// The DAC LUT for analog waveform output + float* wavDAC; //-V730_NOINIT this is initialized in the SID constructor + + /// The DAC LUT for analog envelope output + float* envDAC; //-V730_NOINIT this is initialized in the SID constructor + +public: + /** + * Amplitude modulated waveform output. + * + * The waveform DAC generates a voltage between virtual ground and Vdd + * (5-12 V for the 6581 and 4.75-9 V for the 8580) + * corresponding to oscillator state 0 .. 4095. + * + * The envelope DAC generates a voltage between waveform gen output and + * the virtual ground level, corresponding to envelope state 0 .. 255. + * + * Ideal range [-2048*255, 2047*255]. + * + * @param ringModulator Ring-modulator for waveform + * @return the voice analog output + */ + RESID_INLINE + int output(const WaveformGenerator* ringModulator) const + { + unsigned int const wav = waveformGenerator->output(ringModulator); + unsigned int const env = envelopeGenerator->output(); + + // DAC imperfections are emulated by using the digital output + // as an index into a DAC lookup table. + return static_cast(wavDAC[wav] * envDAC[env]); + } + + /** + * Constructor. + */ + Voice() : + waveformGenerator(new WaveformGenerator()), + envelopeGenerator(new EnvelopeGenerator()) {} + + /** + * Set the analog DAC emulation for waveform generator. + * Must be called before any operation. + * + * @param dac + */ + void setWavDAC(float* dac) { wavDAC = dac; } + + /** + * Set the analog DAC emulation for envelope. + * Must be called before any operation. + * + * @param dac + */ + void setEnvDAC(float* dac) { envDAC = dac; } + + WaveformGenerator* wave() const { return waveformGenerator.get(); } + + EnvelopeGenerator* envelope() const { return envelopeGenerator.get(); } + + /** + * Write control register. + * + * @param control Control register value. + */ + void writeCONTROL_REG(unsigned char control) + { + waveformGenerator->writeCONTROL_REG(control); + envelopeGenerator->writeCONTROL_REG(control); + } + + /** + * SID reset. + */ + void reset() + { + waveformGenerator->reset(); + envelopeGenerator->reset(); + } +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/WaveformCalculator.cpp b/src/engine/platform/sound/c64_fp/WaveformCalculator.cpp new file mode 100644 index 00000000..fe5030fa --- /dev/null +++ b/src/engine/platform/sound/c64_fp/WaveformCalculator.cpp @@ -0,0 +1,204 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2016 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "WaveformCalculator.h" + +#include + +namespace reSIDfp +{ + +WaveformCalculator* WaveformCalculator::getInstance() +{ + static WaveformCalculator instance; + return &instance; +} + +/** + * Parameters derived with the Monte Carlo method based on + * samplings by kevtris. Code and data available in the project repository [1]. + * + * The score here reported is the acoustic error + * calculated XORing the estimated and the sampled values. + * In parentheses the number of mispredicted bits + * on a total of 32768. + * + * [1] https://github.com/libsidplayfp/combined-waveforms + */ +const CombinedWaveformConfig config[2][4] = +{ + { /* kevtris chip G (6581 R2) */ + {0.90251f, 0.f, 0.f, 1.9147f, 1.6747f, 0.62376f }, // error 1689 (280) + {0.93088f, 2.4843f, 0.f, 1.0353f, 1.1484f, 0.f }, // error 6128 (130) + {0.90988f, 2.26303f, 1.13126f, 1.0035f, 1.13801f, 0.f }, // error 14243 (632) + {0.91f, 1.192f, 0.f, 1.0169f, 1.2f, 0.637f }, // error 64 (2) + }, + { /* kevtris chip V (8580 R5) */ + {0.9632f, 0.f, 0.975f, 1.7467f, 2.36132f, 0.975395f}, // error 1380 (169) + {0.92886f, 1.67696f, 0.f, 1.1014f, 1.4352f, 0.f }, // error 8007 (218) + {0.94043f, 1.7937f, 0.981f, 1.1213f, 1.4259f, 0.f }, // error 11957 (362) + {0.96211f, 0.98695f, 1.00387f, 1.46499f, 1.98375f, 0.77777f }, // error 2369 (89) + }, +}; + +/** + * Generate bitstate based on emulation of combined waves. + * + * @param config model parameters matrix + * @param waveform the waveform to emulate, 1 .. 7 + * @param accumulator the high bits of the accumulator value + */ +short calculateCombinedWaveform(const CombinedWaveformConfig& config, int waveform, int accumulator) +{ + float o[12]; + + // Saw + for (unsigned int i = 0; i < 12; i++) + { + o[i] = (accumulator & (1 << i)) != 0 ? 1.f : 0.f; + } + + // convert to Triangle + if ((waveform & 3) == 1) + { + const bool top = (accumulator & 0x800) != 0; + + for (int i = 11; i > 0; i--) + { + o[i] = top ? 1.0f - o[i - 1] : o[i - 1]; + } + + o[0] = 0.f; + } + + // or to Saw+Triangle + else if ((waveform & 3) == 3) + { + // bottom bit is grounded via T waveform selector + o[0] *= config.stmix; + + for (int i = 1; i < 12; i++) + { + /* + * Enabling the S waveform pulls the XOR circuit selector transistor down + * (which would normally make the descending ramp of the triangle waveform), + * so ST does not actually have a sawtooth and triangle waveform combined, + * but merely combines two sawtooths, one rising double the speed the other. + * + * http://www.lemon64.com/forum/viewtopic.php?t=25442&postdays=0&postorder=asc&start=165 + */ + o[i] = o[i - 1] * (1.f - config.stmix) + o[i] * config.stmix; + } + } + + // topbit for Saw + if ((waveform & 2) == 2) + { + o[11] *= config.topbit; + } + + // ST, P* waveforms + if (waveform == 3 || waveform > 4) + { + float distancetable[12 * 2 + 1]; + distancetable[12] = 1.f; + for (int i = 12; i > 0; i--) + { + distancetable[12-i] = 1.0f / pow(config.distance1, i); + distancetable[12+i] = 1.0f / pow(config.distance2, i); + } + + float tmp[12]; + + for (int i = 0; i < 12; i++) + { + float avg = 0.f; + float n = 0.f; + + for (int j = 0; j < 12; j++) + { + const float weight = distancetable[i - j + 12]; + avg += o[j] * weight; + n += weight; + } + + // pulse control bit + if (waveform > 4) + { + const float weight = distancetable[i - 12 + 12]; + avg += config.pulsestrength * weight; + n += weight; + } + + tmp[i] = (o[i] + avg / n) * 0.5f; + } + + for (int i = 0; i < 12; i++) + { + o[i] = tmp[i]; + } + } + + short value = 0; + + for (unsigned int i = 0; i < 12; i++) + { + if (o[i] > config.bias) + { + value |= 1 << i; + } + } + + return value; +} + +matrix_t* WaveformCalculator::buildTable(ChipModel model) +{ + const CombinedWaveformConfig* cfgArray = config[model == MOS6581 ? 0 : 1]; + + cw_cache_t::iterator lb = CACHE.lower_bound(cfgArray); + + if (lb != CACHE.end() && !(CACHE.key_comp()(cfgArray, lb->first))) + { + return &(lb->second); + } + + matrix_t wftable(8, 4096); + + for (unsigned int idx = 0; idx < 1 << 12; idx++) + { + wftable[0][idx] = 0xfff; + wftable[1][idx] = static_cast((idx & 0x800) == 0 ? idx << 1 : (idx ^ 0xfff) << 1); + wftable[2][idx] = static_cast(idx); + wftable[3][idx] = calculateCombinedWaveform(cfgArray[0], 3, idx); + wftable[4][idx] = 0xfff; + wftable[5][idx] = calculateCombinedWaveform(cfgArray[1], 5, idx); + wftable[6][idx] = calculateCombinedWaveform(cfgArray[2], 6, idx); + wftable[7][idx] = calculateCombinedWaveform(cfgArray[3], 7, idx); + } +#ifdef HAVE_CXX11 + return &(CACHE.emplace_hint(lb, cw_cache_t::value_type(cfgArray, wftable))->second); +#else + return &(CACHE.insert(lb, cw_cache_t::value_type(cfgArray, wftable))->second); +#endif +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/WaveformCalculator.h b/src/engine/platform/sound/c64_fp/WaveformCalculator.h new file mode 100644 index 00000000..f9183c5d --- /dev/null +++ b/src/engine/platform/sound/c64_fp/WaveformCalculator.h @@ -0,0 +1,128 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2016 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef WAVEFORMCALCULATOR_h +#define WAVEFORMCALCULATOR_h + +#include + +#include "array.h" +#include "sidcxx11.h" +#include "siddefs-fp.h" + + +namespace reSIDfp +{ + +/** + * Combined waveform model parameters. + */ +typedef struct +{ + float bias; + float pulsestrength; + float topbit; + float distance1; + float distance2; + float stmix; +} CombinedWaveformConfig; + +/** + * Combined waveform calculator for WaveformGenerator. + * By combining waveforms, the bits of each waveform are effectively short + * circuited. A zero bit in one waveform will result in a zero output bit + * (thus the infamous claim that the waveforms are AND'ed). + * However, a zero bit in one waveform may also affect the neighboring bits + * in the output. + * + * Example: + * + * 1 1 + * Bit # 1 0 9 8 7 6 5 4 3 2 1 0 + * ----------------------- + * Sawtooth 0 0 0 1 1 1 1 1 1 0 0 0 + * + * Triangle 0 0 1 1 1 1 1 1 0 0 0 0 + * + * AND 0 0 0 1 1 1 1 1 0 0 0 0 + * + * Output 0 0 0 0 1 1 1 0 0 0 0 0 + * + * + * Re-vectorized die photographs reveal the mechanism behind this behavior. + * Each waveform selector bit acts as a switch, which directly connects + * internal outputs into the waveform DAC inputs as follows: + * + * - Noise outputs the shift register bits to DAC inputs as described above. + * Each output is also used as input to the next bit when the shift register + * is shifted. Lower four bits are grounded. + * - Pulse connects a single line to all DAC inputs. The line is connected to + * either 5V (pulse on) or 0V (pulse off) at bit 11, and ends at bit 0. + * - Triangle connects the upper 11 bits of the (MSB EOR'ed) accumulator to the + * DAC inputs, so that DAC bit 0 = 0, DAC bit n = accumulator bit n - 1. + * - Sawtooth connects the upper 12 bits of the accumulator to the DAC inputs, + * so that DAC bit n = accumulator bit n. Sawtooth blocks out the MSB from + * the EOR used to generate the triangle waveform. + * + * We can thus draw the following conclusions: + * + * - The shift register may be written to by combined waveforms. + * - The pulse waveform interconnects all bits in combined waveforms via the + * pulse line. + * - The combination of triangle and sawtooth interconnects neighboring bits + * of the sawtooth waveform. + * + * Also in the 6581 the MSB of the oscillator, used as input for the + * triangle xor logic and the pulse adder's last bit, is connected directly + * to the waveform selector, while in the 8580 it is latched at sid_clk2 + * before being forwarded to the selector. Thus in the 6581 if the sawtooth MSB + * is pulled down it might affect the oscillator's adder + * driving the top bit low. + * + */ +class WaveformCalculator +{ +private: + typedef std::map cw_cache_t; + +private: + cw_cache_t CACHE; + + WaveformCalculator() DEFAULT; + +public: + /** + * Get the singleton instance. + */ + static WaveformCalculator* getInstance(); + + /** + * Build waveform tables for use by WaveformGenerator. + * + * @param model Chip model to use + * @return Waveform table + */ + matrix_t* buildTable(ChipModel model); +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/WaveformGenerator.cpp b/src/engine/platform/sound/c64_fp/WaveformGenerator.cpp new file mode 100644 index 00000000..16a9eb6b --- /dev/null +++ b/src/engine/platform/sound/c64_fp/WaveformGenerator.cpp @@ -0,0 +1,357 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2021 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define WAVEFORMGENERATOR_CPP + +#include "WaveformGenerator.h" + +/* + * This fixes tests + * SID/wb_testsuite/noise_writeback_check_8_to_C_old + * SID/wb_testsuite/noise_writeback_check_9_to_C_old + * SID/wb_testsuite/noise_writeback_check_A_to_C_old + * SID/wb_testsuite/noise_writeback_check_C_to_C_old + * + * but breaks SID/wf12nsr/wf12nsr + * + * needs more digging... + */ +//#define NO_WB_NOI_PUL + +namespace reSIDfp +{ + +/** + * Number of cycles after which the waveform output fades to 0 when setting + * the waveform register to 0. + * Values measured on warm chips (6581R3/R4 and 8580R5) + * checking OSC3. + * Times vary wildly with temperature and may differ + * from chip to chip so the numbers here represent + * only the big difference between the old and new models. + * + * See [VICE Bug #290](http://sourceforge.net/p/vice-emu/bugs/290/) + * and [VICE Bug #1128](http://sourceforge.net/p/vice-emu/bugs/1128/) + */ +// ~95ms +const unsigned int FLOATING_OUTPUT_TTL_6581R3 = 54000; +const unsigned int FLOATING_OUTPUT_FADE_6581R3 = 1400; +// ~1s +const unsigned int FLOATING_OUTPUT_TTL_6581R4 = 1000000; +// ~1s +const unsigned int FLOATING_OUTPUT_TTL_8580R5 = 800000; +const unsigned int FLOATING_OUTPUT_FADE_8580R5 = 50000; + +/** + * Number of cycles after which the shift register is reset + * when the test bit is set. + * Values measured on warm chips (6581R3/R4 and 8580R5) + * checking OSC3. + * Times vary wildly with temperature and may differ + * from chip to chip so the numbers here represent + * only the big difference between the old and new models. + */ +// ~210ms +const unsigned int SHIFT_REGISTER_RESET_6581R3 = 50000; +const unsigned int SHIFT_REGISTER_FADE_6581R3 = 15000; +// ~2.15s +const unsigned int SHIFT_REGISTER_RESET_6581R4 = 2150000; +// ~2.8s +const unsigned int SHIFT_REGISTER_RESET_8580R5 = 986000; +const unsigned int SHIFT_REGISTER_FADE_8580R5 = 314300; + +/* + * This is what happens when the lfsr is clocked: + * + * cycle 0: bit 19 of the accumulator goes from low to high, the noise register acts normally, + * the output may overwrite a bit; + * + * cycle 1: first phase of the shift, the bits are interconnected and the output of each bit + * is latched into the following. The output may overwrite the latched value. + * + * cycle 2: second phase of the shift, the latched value becomes active in the first + * half of the clock and from the second half the register returns to normal operation. + * + * When the test or reset lines are active the first phase is executed at every cyle + * until the signal is released triggering the second phase. + */ +void WaveformGenerator::clock_shift_register(unsigned int bit0) +{ + shift_register = (shift_register >> 1) | bit0; + + // New noise waveform output. + set_noise_output(); +} + +unsigned int WaveformGenerator::get_noise_writeback() +{ + return + ~( + (1 << 2) | // Bit 20 + (1 << 4) | // Bit 18 + (1 << 8) | // Bit 14 + (1 << 11) | // Bit 11 + (1 << 13) | // Bit 9 + (1 << 17) | // Bit 5 + (1 << 20) | // Bit 2 + (1 << 22) // Bit 0 + ) | + ((waveform_output & (1 << 11)) >> 9) | // Bit 11 -> bit 20 + ((waveform_output & (1 << 10)) >> 6) | // Bit 10 -> bit 18 + ((waveform_output & (1 << 9)) >> 1) | // Bit 9 -> bit 14 + ((waveform_output & (1 << 8)) << 3) | // Bit 8 -> bit 11 + ((waveform_output & (1 << 7)) << 6) | // Bit 7 -> bit 9 + ((waveform_output & (1 << 6)) << 11) | // Bit 6 -> bit 5 + ((waveform_output & (1 << 5)) << 15) | // Bit 5 -> bit 2 + ((waveform_output & (1 << 4)) << 18); // Bit 4 -> bit 0 +} + +void WaveformGenerator::write_shift_register() +{ + if (unlikely(waveform > 0x8) && likely(!test) && likely(shift_pipeline != 1)) + { + // Write changes to the shift register output caused by combined waveforms + // back into the shift register. This happens only when the register is clocked + // (see $D1+$81_wave_test [1]) or when the test bit is falling. + // A bit once set to zero cannot be changed, hence the and'ing. + // + // [1] ftp://ftp.untergrund.net/users/nata/sid_test/$D1+$81_wave_test.7z + // + // FIXME: Write test program to check the effect of 1 bits and whether + // neighboring bits are affected. + +#ifdef NO_WB_NOI_PUL + if (waveform == 0xc) + return; +#endif + shift_register &= get_noise_writeback(); + + noise_output &= waveform_output; + set_no_noise_or_noise_output(); + } +} + +void WaveformGenerator::set_noise_output() +{ + noise_output = + ((shift_register & (1 << 2)) << 9) | // Bit 20 -> bit 11 + ((shift_register & (1 << 4)) << 6) | // Bit 18 -> bit 10 + ((shift_register & (1 << 8)) << 1) | // Bit 14 -> bit 9 + ((shift_register & (1 << 11)) >> 3) | // Bit 11 -> bit 8 + ((shift_register & (1 << 13)) >> 6) | // Bit 9 -> bit 7 + ((shift_register & (1 << 17)) >> 11) | // Bit 5 -> bit 6 + ((shift_register & (1 << 20)) >> 15) | // Bit 2 -> bit 5 + ((shift_register & (1 << 22)) >> 18); // Bit 0 -> bit 4 + + set_no_noise_or_noise_output(); +} + +void WaveformGenerator::setWaveformModels(matrix_t* models) +{ + model_wave = models; +} + +void WaveformGenerator::synchronize(WaveformGenerator* syncDest, const WaveformGenerator* syncSource) const +{ + // A special case occurs when a sync source is synced itself on the same + // cycle as when its MSB is set high. In this case the destination will + // not be synced. This has been verified by sampling OSC3. + if (unlikely(msb_rising) && syncDest->sync && !(sync && syncSource->msb_rising)) + { + syncDest->accumulator = 0; + } +} + +bool do_pre_writeback(unsigned int waveform_prev, unsigned int waveform, bool is6581) +{ + // no writeback without combined waveforms + if (likely(waveform_prev <= 0x8)) + return false; + // no writeback when changing to noise + if (waveform == 8) + return false; + // What's happening here? + if (is6581 && + ((((waveform_prev & 0x3) == 0x1) && ((waveform & 0x3) == 0x2)) + || (((waveform_prev & 0x3) == 0x2) && ((waveform & 0x3) == 0x1)))) + return false; + if (waveform_prev == 0xc) + { + if (is6581) + return false; + else if ((waveform != 0x9) && (waveform != 0xe)) + return false; + } +#ifdef NO_WB_NOI_PUL + if (waveform == 0xc) + return false; +#endif + // ok do the writeback + return true; +} + +/* + * When noise and pulse are combined all the bits are + * connected and the four lower ones are grounded. + * This causes the adjacent bits to be pulled down, + * with different strength depending on model. + * + * This is just a rough attempt at modelling the effect. + */ + +static unsigned int noise_pulse6581(unsigned int noise) +{ + return (noise < 0xf00) ? 0x000 : noise & (noise << 1) & (noise << 2); +} + +static unsigned int noise_pulse8580(unsigned int noise) +{ + return (noise < 0xfc0) ? noise & (noise << 1) : 0xfc0; +} + +void WaveformGenerator::set_no_noise_or_noise_output() +{ + no_noise_or_noise_output = no_noise | noise_output; + + // pulse+noise + if (unlikely((waveform & 0xc) == 0xc)) + no_noise_or_noise_output = is6581 + ? noise_pulse6581(no_noise_or_noise_output) + : noise_pulse8580(no_noise_or_noise_output); + +} + +void WaveformGenerator::writeCONTROL_REG(unsigned char control) +{ + const unsigned int waveform_prev = waveform; + const bool test_prev = test; + + waveform = (control >> 4) & 0x0f; + test = (control & 0x08) != 0; + sync = (control & 0x02) != 0; + + // Substitution of accumulator MSB when sawtooth = 0, ring_mod = 1. + ring_msb_mask = ((~control >> 5) & (control >> 2) & 0x1) << 23; + + if (waveform != waveform_prev) + { + // Set up waveform table. + wave = (*model_wave)[waveform & 0x7]; + + // no_noise and no_pulse are used in set_waveform_output() as bitmasks to + // only let the noise or pulse influence the output when the noise or pulse + // waveforms are selected. + no_noise = (waveform & 0x8) != 0 ? 0x000 : 0xfff; + set_no_noise_or_noise_output(); + no_pulse = (waveform & 0x4) != 0 ? 0x000 : 0xfff; + + if (waveform == 0) + { + // Change to floating DAC input. + // Reset fading time for floating DAC input. + floating_output_ttl = is6581 ? FLOATING_OUTPUT_TTL_6581R3 : FLOATING_OUTPUT_TTL_8580R5; + } + } + + if (test != test_prev) + { + if (test) + { + // Reset accumulator. + accumulator = 0; + + // Flush shift pipeline. + shift_pipeline = 0; + + // Set reset time for shift register. + shift_register_reset = is6581 ? SHIFT_REGISTER_RESET_6581R3 : SHIFT_REGISTER_RESET_8580R5; + } + else + { + // When the test bit is falling, the second phase of the shift is + // completed by enabling SRAM write. + + // During first phase of the shift the bits are interconnected + // and the output of each bit is latched into the following. + // The output may overwrite the latched value. + if (do_pre_writeback(waveform_prev, waveform, is6581)) + { + shift_register &= get_noise_writeback(); + } + + // bit0 = (bit22 | test) ^ bit17 = 1 ^ bit17 = ~bit17 + clock_shift_register((~shift_register << 17) & (1 << 22)); + } + } +} + +void WaveformGenerator::waveBitfade() +{ + waveform_output &= waveform_output >> 1; + osc3 = waveform_output; + if (waveform_output != 0) + floating_output_ttl = is6581 ? FLOATING_OUTPUT_FADE_6581R3 : FLOATING_OUTPUT_FADE_8580R5; +} + +void WaveformGenerator::shiftregBitfade() +{ + shift_register |= shift_register >> 1; + shift_register |= 0x400000; + if (shift_register != 0x7fffff) + shift_register_reset = is6581 ? SHIFT_REGISTER_FADE_6581R3 : SHIFT_REGISTER_FADE_8580R5; +} + +void WaveformGenerator::reset() +{ + // accumulator is not changed on reset + freq = 0; + pw = 0; + + msb_rising = false; + + waveform = 0; + osc3 = 0; + + test = false; + sync = false; + + wave = model_wave ? (*model_wave)[0] : nullptr; + + ring_msb_mask = 0; + no_noise = 0xfff; + no_pulse = 0xfff; + pulse_output = 0xfff; + + shift_register_reset = 0; + shift_register = 0x7fffff; + // when reset is released the shift register is clocked once + // so the lower bit is zeroed out + // bit0 = (bit22 | test) ^ bit17 = 1 ^ 1 = 0 + clock_shift_register(0); + + shift_pipeline = 0; + + waveform_output = 0; + floating_output_ttl = 0; +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/WaveformGenerator.h b/src/engine/platform/sound/c64_fp/WaveformGenerator.h new file mode 100644 index 00000000..9fd617f6 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/WaveformGenerator.h @@ -0,0 +1,396 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2022 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004,2010 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef WAVEFORMGENERATOR_H +#define WAVEFORMGENERATOR_H + +#include "siddefs-fp.h" +#include "array.h" + +#include "sidcxx11.h" + +namespace reSIDfp +{ + +/** + * A 24 bit accumulator is the basis for waveform generation. + * FREQ is added to the lower 16 bits of the accumulator each cycle. + * The accumulator is set to zero when TEST is set, and starts counting + * when TEST is cleared. + * + * Waveforms are generated as follows: + * + * - No waveform: + * When no waveform is selected, the DAC input is floating. + * + * + * - Triangle: + * The upper 12 bits of the accumulator are used. + * The MSB is used to create the falling edge of the triangle by inverting + * the lower 11 bits. The MSB is thrown away and the lower 11 bits are + * left-shifted (half the resolution, full amplitude). + * Ring modulation substitutes the MSB with MSB EOR NOT sync_source MSB. + * + * + * - Sawtooth: + * The output is identical to the upper 12 bits of the accumulator. + * + * + * - Pulse: + * The upper 12 bits of the accumulator are used. + * These bits are compared to the pulse width register by a 12 bit digital + * comparator; output is either all one or all zero bits. + * The pulse setting is delayed one cycle after the compare. + * The test bit, when set to one, holds the pulse waveform output at 0xfff + * regardless of the pulse width setting. + * + * + * - Noise: + * The noise output is taken from intermediate bits of a 23-bit shift register + * which is clocked by bit 19 of the accumulator. + * The shift is delayed 2 cycles after bit 19 is set high. + * + * Operation: Calculate EOR result, shift register, set bit 0 = result. + * + * reset +--------------------------------------------+ + * | | | + * test--OR-->EOR<--+ | + * | | | + * 2 2 2 1 1 1 1 1 1 1 1 1 1 | + * Register bits: 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 <---+ + * | | | | | | | | + * Waveform bits: 1 1 9 8 7 6 5 4 + * 1 0 + * + * The low 4 waveform bits are zero (grounded). + */ +class WaveformGenerator +{ +private: + matrix_t* model_wave; + + short* wave; + + // PWout = (PWn/40.95)% + unsigned int pw; + + unsigned int shift_register; + + /// Emulation of pipeline causing bit 19 to clock the shift register. + int shift_pipeline; + + unsigned int ring_msb_mask; + unsigned int no_noise; + unsigned int noise_output; + unsigned int no_noise_or_noise_output; + unsigned int no_pulse; + unsigned int pulse_output; + + /// The control register right-shifted 4 bits; used for output function table lookup. + unsigned int waveform; + + unsigned int waveform_output; + + /// Current accumulator value. + unsigned int accumulator; + + // Fout = (Fn*Fclk/16777216)Hz + unsigned int freq; + + /// 8580 tri/saw pipeline + unsigned int tri_saw_pipeline; + + /// The OSC3 value + unsigned int osc3; + + /// Remaining time to fully reset shift register. + unsigned int shift_register_reset; + + // The wave signal TTL when no waveform is selected + unsigned int floating_output_ttl; + + /// The control register bits. Gate is handled by EnvelopeGenerator. + //@{ + bool test; + bool sync; + //@} + + /// Tell whether the accumulator MSB was set high on this cycle. + bool msb_rising; + + bool is6581; //-V730_NOINIT this is initialized in the SID constructor + +private: + void clock_shift_register(unsigned int bit0); + + unsigned int get_noise_writeback(); + + void write_shift_register(); + + void set_noise_output(); + + void set_no_noise_or_noise_output(); + + void waveBitfade(); + + void shiftregBitfade(); + +public: + void setWaveformModels(matrix_t* models); + + /** + * Set the chip model. + * Must be called before any operation. + * + * @param is6581 true if MOS6581, false if CSG8580 + */ + void setModel(bool is6581) { this->is6581 = is6581; } + + /** + * SID clocking. + */ + void clock(); + + /** + * Synchronize oscillators. + * This must be done after all the oscillators have been clock()'ed, + * so that they are in the same state. + * + * @param syncDest The oscillator that will be synced + * @param syncSource The sync source oscillator + */ + void synchronize(WaveformGenerator* syncDest, const WaveformGenerator* syncSource) const; + + /** + * Constructor. + */ + WaveformGenerator() : + model_wave(nullptr), + wave(nullptr), + pw(0), + shift_register(0), + shift_pipeline(0), + ring_msb_mask(0), + no_noise(0), + noise_output(0), + no_noise_or_noise_output(0), + no_pulse(0), + pulse_output(0), + waveform(0), + waveform_output(0), + accumulator(0x555555), // Accumulator's even bits are high on powerup + freq(0), + tri_saw_pipeline(0x555), + osc3(0), + shift_register_reset(0), + floating_output_ttl(0), + test(false), + sync(false), + msb_rising(false) {} + + /** + * Write FREQ LO register. + * + * @param freq_lo low 8 bits of frequency + */ + void writeFREQ_LO(unsigned char freq_lo) { freq = (freq & 0xff00) | (freq_lo & 0xff); } + + /** + * Write FREQ HI register. + * + * @param freq_hi high 8 bits of frequency + */ + void writeFREQ_HI(unsigned char freq_hi) { freq = (freq_hi << 8 & 0xff00) | (freq & 0xff); } + + /** + * Write PW LO register. + * + * @param pw_lo low 8 bits of pulse width + */ + void writePW_LO(unsigned char pw_lo) { pw = (pw & 0xf00) | (pw_lo & 0x0ff); } + + /** + * Write PW HI register. + * + * @param pw_hi high 8 bits of pulse width + */ + void writePW_HI(unsigned char pw_hi) { pw = (pw_hi << 8 & 0xf00) | (pw & 0x0ff); } + + /** + * Write CONTROL REGISTER register. + * + * @param control control register value + */ + void writeCONTROL_REG(unsigned char control); + + /** + * SID reset. + */ + void reset(); + + /** + * 12-bit waveform output. + * + * @param ringModulator The oscillator ring-modulating current one. + * @return the waveform generator digital output + */ + unsigned int output(const WaveformGenerator* ringModulator); + + /** + * Read OSC3 value. + */ + unsigned char readOSC() const { return static_cast(osc3 >> 4); } + + /** + * Read accumulator value. + */ + unsigned int readAccumulator() const { return accumulator; } + + /** + * Read freq value. + */ + unsigned int readFreq() const { return freq; } + + /** + * Read test value. + */ + bool readTest() const { return test; } + + /** + * Read sync value. + */ + bool readSync() const { return sync; } +}; + +} // namespace reSIDfp + +#if RESID_INLINING || defined(WAVEFORMGENERATOR_CPP) + +namespace reSIDfp +{ + +RESID_INLINE +void WaveformGenerator::clock() +{ + if (unlikely(test)) + { + if (unlikely(shift_register_reset != 0) && unlikely(--shift_register_reset == 0)) + { + shiftregBitfade(); + + // New noise waveform output. + set_noise_output(); + } + + // The test bit sets pulse high. + pulse_output = 0xfff; + } + else + { + // Calculate new accumulator value; + const unsigned int accumulator_old = accumulator; + accumulator = (accumulator + freq) & 0xffffff; + + // Check which bit have changed + const unsigned int accumulator_bits_set = ~accumulator_old & accumulator; + + // Check whether the MSB is set high. This is used for synchronization. + msb_rising = (accumulator_bits_set & 0x800000) != 0; + + // Shift noise register once for each time accumulator bit 19 is set high. + // The shift is delayed 2 cycles. + if (unlikely((accumulator_bits_set & 0x080000) != 0)) + { + // Pipeline: Detect rising bit, shift phase 1, shift phase 2. + shift_pipeline = 2; + } + else if (unlikely(shift_pipeline != 0) && --shift_pipeline == 0) + { + // bit0 = (bit22 | test) ^ bit17 + clock_shift_register(((shift_register << 22) ^ (shift_register << 17)) & (1 << 22)); + } + } +} + +RESID_INLINE +unsigned int WaveformGenerator::output(const WaveformGenerator* ringModulator) +{ + // Set output value. + if (likely(waveform != 0)) + { + const unsigned int ix = (accumulator ^ (~ringModulator->accumulator & ring_msb_mask)) >> 12; + + // The bit masks no_pulse and no_noise are used to achieve branch-free + // calculation of the output value. + waveform_output = wave[ix] & (no_pulse | pulse_output) & no_noise_or_noise_output; + + // Triangle/Sawtooth output is delayed half cycle on 8580. + // This will appear as a one cycle delay on OSC3 as it is latched first phase of the clock. + if ((waveform & 3) && !is6581) + { + osc3 = tri_saw_pipeline & (no_pulse | pulse_output) & no_noise_or_noise_output; + tri_saw_pipeline = wave[ix]; + } + else + { + osc3 = waveform_output; + } + + // In the 6581 the top bit of the accumulator may be driven low by combined waveforms + // when the sawtooth is selected + // FIXME doesn't seem to always happen + if ((waveform & 2) && unlikely(waveform & 0xd) && is6581) + accumulator &= (waveform_output << 12) | 0x7fffff; + + write_shift_register(); + } + else + { + // Age floating DAC input. + if (likely(floating_output_ttl != 0) && unlikely(--floating_output_ttl == 0)) + { + waveBitfade(); + } + } + + // The pulse level is defined as (accumulator >> 12) >= pw ? 0xfff : 0x000. + // The expression -((accumulator >> 12) >= pw) & 0xfff yields the same + // results without any branching (and thus without any pipeline stalls). + // NB! This expression relies on that the result of a boolean expression + // is either 0 or 1, and furthermore requires two's complement integer. + // A few more cycles may be saved by storing the pulse width left shifted + // 12 bits, and dropping the and with 0xfff (this is valid since pulse is + // used as a bit mask on 12 bit values), yielding the expression + // -(accumulator >= pw24). However this only results in negligible savings. + + // The result of the pulse width compare is delayed one cycle. + // Push next pulse level into pulse level pipeline. + pulse_output = ((accumulator >> 12) >= pw) ? 0xfff : 0x000; + + return waveform_output; +} + +} // namespace reSIDfp + +#endif + +#endif diff --git a/src/engine/platform/sound/c64_fp/array.h b/src/engine/platform/sound/c64_fp/array.h new file mode 100644 index 00000000..5291938c --- /dev/null +++ b/src/engine/platform/sound/c64_fp/array.h @@ -0,0 +1,73 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright (C) 2011-2014 Leandro Nini + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef ARRAY_H +#define ARRAY_H + +/** + * Counter. + */ +class counter +{ +private: + unsigned int c; + +public: + counter() : c(1) {} + void increase() { ++c; } + unsigned int decrease() { return --c; } +}; + +/** + * Reference counted pointer to matrix wrapper, for use with standard containers. + */ +template +class matrix +{ +private: + T* data; + counter* count; + const unsigned int x, y; + +public: + matrix(unsigned int x, unsigned int y) : + data(new T[x * y]), + count(new counter()), + x(x), + y(y) {} + + matrix(const matrix& p) : + data(p.data), + count(p.count), + x(p.x), + y(p.y) { count->increase(); } + + ~matrix() { if (count->decrease() == 0) { delete count; delete [] data; } } + + unsigned int length() const { return x * y; } + + T* operator[](unsigned int a) { return &data[a * y]; } + + T const* operator[](unsigned int a) const { return &data[a * y]; } +}; + +typedef matrix matrix_t; + +#endif diff --git a/src/engine/platform/sound/c64_fp/resample/Resampler.h b/src/engine/platform/sound/c64_fp/resample/Resampler.h new file mode 100644 index 00000000..904f6545 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/resample/Resampler.h @@ -0,0 +1,86 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2020 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef RESAMPLER_H +#define RESAMPLER_H + +#include + +#include "../sidcxx11.h" + +#include "../siddefs-fp.h" + +namespace reSIDfp +{ + +/** + * Abstraction of a resampling process. Given enough input, produces output. + * Constructors take additional arguments that configure these objects. + */ +class Resampler +{ +protected: + inline short softClip(int x) const + { + constexpr int threshold = 28000; + if (likely(x < threshold)) + return x; + + constexpr double t = threshold / 32768.; + constexpr double a = 1. - t; + constexpr double b = 1. / a; + + double value = static_cast(x - threshold) / 32768.; + value = t + a * tanh(b * value); + return static_cast(value * 32768.); + } + + virtual int output() const = 0; + + Resampler() {} + +public: + virtual ~Resampler() {} + + /** + * Input a sample into resampler. Output "true" when resampler is ready with new sample. + * + * @param sample input sample + * @return true when a sample is ready + */ + virtual bool input(int sample) = 0; + + /** + * Output a sample from resampler. + * + * @return resampled sample + */ + short getOutput() const + { + return softClip(output()); + } + + virtual void reset() = 0; +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/resample/SincResampler.cpp b/src/engine/platform/sound/c64_fp/resample/SincResampler.cpp new file mode 100755 index 00000000..adb17f9e --- /dev/null +++ b/src/engine/platform/sound/c64_fp/resample/SincResampler.cpp @@ -0,0 +1,393 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2020 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "SincResampler.h" + +#include +#include +#include +#include +#include + +#include "../siddefs-fp.h" + +#ifdef HAVE_EMMINTRIN_H +# include +#elif defined HAVE_MMINTRIN_H +# include +#elif defined(HAVE_ARM_NEON_H) +# include +#endif + +namespace reSIDfp +{ + +typedef std::map fir_cache_t; + +/// Cache for the expensive FIR table computation results. +fir_cache_t FIR_CACHE; + +/// Maximum error acceptable in I0 is 1e-6, or ~96 dB. +const double I0E = 1e-6; + +const int BITS = 16; + +/** + * Compute the 0th order modified Bessel function of the first kind. + * This function is originally from resample-1.5/filterkit.c by J. O. Smith. + * It is used to build the Kaiser window for resampling. + * + * @param x evaluate I0 at x + * @return value of I0 at x. + */ +double I0(double x) +{ + double sum = 1.; + double u = 1.; + double n = 1.; + const double halfx = x / 2.; + + do + { + const double temp = halfx / n; + u *= temp * temp; + sum += u; + n += 1.; + } + while (u >= I0E * sum); + + return sum; +} + +/** + * Calculate convolution with sample and sinc. + * + * @param a sample buffer input + * @param b sinc buffer + * @param bLength length of the sinc buffer + * @return convolved result + */ +int convolve(const short* a, const short* b, int bLength) +{ +#ifdef HAVE_EMMINTRIN_H + int out = 0; + + const uintptr_t offset = (uintptr_t)(a) & 0x0f; + + // check for aligned accesses + if (offset == ((uintptr_t)(b) & 0x0f)) + { + if (offset) + { + const int l = (0x10 - offset)/2; + + for (int i = 0; i < l; i++) + { + out += *a++ * *b++; + } + + bLength -= offset; + } + + __m128i acc = _mm_setzero_si128(); + + const int n = bLength / 8; + + for (int i = 0; i < n; i++) + { + const __m128i tmp = _mm_madd_epi16(*(__m128i*)a, *(__m128i*)b); + acc = _mm_add_epi16(acc, tmp); + a += 8; + b += 8; + } + + __m128i vsum = _mm_add_epi32(acc, _mm_srli_si128(acc, 8)); + vsum = _mm_add_epi32(vsum, _mm_srli_si128(vsum, 4)); + out += _mm_cvtsi128_si32(vsum); + + bLength &= 7; + } +#elif defined HAVE_MMINTRIN_H + __m64 acc = _mm_setzero_si64(); + + const int n = bLength / 4; + + for (int i = 0; i < n; i++) + { + const __m64 tmp = _mm_madd_pi16(*(__m64*)a, *(__m64*)b); + acc = _mm_add_pi16(acc, tmp); + a += 4; + b += 4; + } + + int out = _mm_cvtsi64_si32(acc) + _mm_cvtsi64_si32(_mm_srli_si64(acc, 32)); + _mm_empty(); + + bLength &= 3; +#elif defined(HAVE_ARM_NEON_H) +#if (defined(__arm64__) && defined(__APPLE__)) || defined(__aarch64__) + int32x4_t acc1Low = vdupq_n_s32(0); + int32x4_t acc1High = vdupq_n_s32(0); + int32x4_t acc2Low = vdupq_n_s32(0); + int32x4_t acc2High = vdupq_n_s32(0); + + const int n = bLength / 16; + + for (int i = 0; i < n; i++) + { + int16x8_t v11 = vld1q_s16(a); + int16x8_t v12 = vld1q_s16(a + 8); + int16x8_t v21 = vld1q_s16(b); + int16x8_t v22 = vld1q_s16(b + 8); + + acc1Low = vmlal_s16(acc1Low, vget_low_s16(v11), vget_low_s16(v21)); + acc1High = vmlal_high_s16(acc1High, v11, v21); + acc2Low = vmlal_s16(acc2Low, vget_low_s16(v12), vget_low_s16(v22)); + acc2High = vmlal_high_s16(acc2High, v12, v22); + + a += 16; + b += 16; + } + + bLength &= 15; + + if (bLength >= 8) + { + int16x8_t v1 = vld1q_s16(a); + int16x8_t v2 = vld1q_s16(b); + + acc1Low = vmlal_s16(acc1Low, vget_low_s16(v1), vget_low_s16(v2)); + acc1High = vmlal_high_s16(acc1High, v1, v2); + + a += 8; + b += 8; + } + + bLength &= 7; + + if (bLength >= 4) + { + int16x4_t v1 = vld1_s16(a); + int16x4_t v2 = vld1_s16(b); + + acc1Low = vmlal_s16(acc1Low, v1, v2); + + a += 4; + b += 4; + } + + int32x4_t accSumsNeon = vaddq_s32(acc1Low, acc1High); + accSumsNeon = vaddq_s32(accSumsNeon, acc2Low); + accSumsNeon = vaddq_s32(accSumsNeon, acc2High); + + int out = vaddvq_s32(accSumsNeon); + + bLength &= 3; +#else + int32x4_t acc = vdupq_n_s32(0); + + const int n = bLength / 4; + + for (int i = 0; i < n; i++) + { + const int16x4_t h_vec = vld1_s16(a); + const int16x4_t x_vec = vld1_s16(b); + acc = vmlal_s16(acc, h_vec, x_vec); + a += 4; + b += 4; + } + + int out = vgetq_lane_s32(acc, 0) + + vgetq_lane_s32(acc, 1) + + vgetq_lane_s32(acc, 2) + + vgetq_lane_s32(acc, 3); + + bLength &= 3; +#endif +#else + int out = 0; +#endif + + for (int i = 0; i < bLength; i++) + { + out += *a++ * *b++; + } + + return (out + (1 << 14)) >> 15; +} + +int SincResampler::fir(int subcycle) +{ + // Find the first of the nearest fir tables close to the phase + int firTableFirst = (subcycle * firRES >> 10); + const int firTableOffset = (subcycle * firRES) & 0x3ff; + + // Find firN most recent samples, plus one extra in case the FIR wraps. + int sampleStart = sampleIndex - firN + RINGSIZE - 1; + + const int v1 = convolve(sample + sampleStart, (*firTable)[firTableFirst], firN); + + // Use next FIR table, wrap around to first FIR table using + // previous sample. + if (unlikely(++firTableFirst == firRES)) + { + firTableFirst = 0; + ++sampleStart; + } + + const int v2 = convolve(sample + sampleStart, (*firTable)[firTableFirst], firN); + + // Linear interpolation between the sinc tables yields good + // approximation for the exact value. + return v1 + (firTableOffset * (v2 - v1) >> 10); +} + +SincResampler::SincResampler(double clockFrequency, double samplingFrequency, double highestAccurateFrequency) : + sampleIndex(0), + cyclesPerSample(static_cast(clockFrequency / samplingFrequency * 1024.)), + sampleOffset(0), + outputValue(0) +{ + // 16 bits -> -96dB stopband attenuation. + const double A = -20. * log10(1.0 / (1 << BITS)); + // A fraction of the bandwidth is allocated to the transition band, which we double + // because we design the filter to transition halfway at nyquist. + const double dw = (1. - 2.*highestAccurateFrequency / samplingFrequency) * M_PI * 2.; + + // For calculation of beta and N see the reference for the kaiserord + // function in the MATLAB Signal Processing Toolbox: + // http://www.mathworks.com/help/signal/ref/kaiserord.html + const double beta = 0.1102 * (A - 8.7); + const double I0beta = I0(beta); + const double cyclesPerSampleD = clockFrequency / samplingFrequency; + + { + // The filter order will maximally be 124 with the current constraints. + // N >= (96.33 - 7.95)/(2 * pi * 2.285 * (maxfreq - passbandfreq) >= 123 + // The filter order is equal to the number of zero crossings, i.e. + // it should be an even number (sinc is symmetric with respect to x = 0). + int N = static_cast((A - 7.95) / (2.285 * dw) + 0.5); + N += N & 1; + + // The filter length is equal to the filter order + 1. + // The filter length must be an odd number (sinc is symmetric with respect to + // x = 0). + firN = static_cast(N * cyclesPerSampleD) + 1; + firN |= 1; + + // Check whether the sample ring buffer would overflow. + assert(firN < RINGSIZE); + + // Error is bounded by err < 1.234 / L^2, so L = sqrt(1.234 / (2^-16)) = sqrt(1.234 * 2^16). + firRES = static_cast(ceil(sqrt(1.234 * (1 << BITS)) / cyclesPerSampleD)); + + // firN*firRES represent the total resolution of the sinc sampling. JOS + // recommends a length of 2^BITS, but we don't quite use that good a filter. + // The filter test program indicates that the filter performs well, though. + } + + // Create the map key + std::ostringstream o; + o << firN << "," << firRES << "," << cyclesPerSampleD; + const std::string firKey = o.str(); + fir_cache_t::iterator lb = FIR_CACHE.lower_bound(firKey); + + // The FIR computation is expensive and we set sampling parameters often, but + // from a very small set of choices. Thus, caching is used to speed initialization. + if (lb != FIR_CACHE.end() && !(FIR_CACHE.key_comp()(firKey, lb->first))) + { + firTable = &(lb->second); + } + else + { + // Allocate memory for FIR tables. + matrix_t tempTable(firRES, firN); +#ifdef HAVE_CXX11 + firTable = &(FIR_CACHE.emplace_hint(lb, fir_cache_t::value_type(firKey, tempTable))->second); +#else + firTable = &(FIR_CACHE.insert(lb, fir_cache_t::value_type(firKey, tempTable))->second); +#endif + + // The cutoff frequency is midway through the transition band, in effect the same as nyquist. + const double wc = M_PI; + + // Calculate the sinc tables. + const double scale = 32768.0 * wc / cyclesPerSampleD / M_PI; + + // we're not interested in the fractional part + // so use int division before converting to double + const int tmp = firN / 2; + const double firN_2 = static_cast(tmp); + + for (int i = 0; i < firRES; i++) + { + const double jPhase = (double) i / firRES + firN_2; + + for (int j = 0; j < firN; j++) + { + const double x = j - jPhase; + + const double xt = x / firN_2; + const double kaiserXt = fabs(xt) < 1. ? I0(beta * sqrt(1. - xt * xt)) / I0beta : 0.; + + const double wt = wc * x / cyclesPerSampleD; + const double sincWt = fabs(wt) >= 1e-8 ? sin(wt) / wt : 1.; + + (*firTable)[i][j] = static_cast(scale * sincWt * kaiserXt); + } + } + } +} + +bool SincResampler::input(int input) +{ + bool ready = false; + + /* + * Clip the input as it may overflow the 16 bit range. + * + * Approximate measured input ranges: + * 6581: [-24262,+25080] (Kawasaki_Synthesizer_Demo) + * 8580: [-21514,+35232] (64_Forever, Drum_Fool) + */ + sample[sampleIndex] = sample[sampleIndex + RINGSIZE] = softClip(input); + sampleIndex = (sampleIndex + 1) & (RINGSIZE - 1); + + if (sampleOffset < 1024) + { + outputValue = fir(sampleOffset); + ready = true; + sampleOffset += cyclesPerSample; + } + + sampleOffset -= 1024; + + return ready; +} + +void SincResampler::reset() +{ + memset(sample, 0, sizeof(sample)); + sampleOffset = 0; +} + +} // namespace reSIDfp diff --git a/src/engine/platform/sound/c64_fp/resample/SincResampler.h b/src/engine/platform/sound/c64_fp/resample/SincResampler.h new file mode 100644 index 00000000..7502d96f --- /dev/null +++ b/src/engine/platform/sound/c64_fp/resample/SincResampler.h @@ -0,0 +1,114 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2013 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * Copyright 2004 Dag Lem + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef SINCRESAMPLER_H +#define SINCRESAMPLER_H + +#include "Resampler.h" + +#include +#include + +#include "../array.h" + +#include "../sidcxx11.h" + +namespace reSIDfp +{ + +/** + * This is the theoretically correct (and computationally intensive) audio sample generation. + * The samples are generated by resampling to the specified sampling frequency. + * The work rate is inversely proportional to the percentage of the bandwidth + * allocated to the filter transition band. + * + * This implementation is based on the paper "A Flexible Sampling-Rate Conversion Method", + * by J. O. Smith and P. Gosset, or rather on the expanded tutorial on the + * [Digital Audio Resampling Home Page](http://www-ccrma.stanford.edu/~jos/resample/). + * + * By building shifted FIR tables with samples according to the sampling frequency, + * this implementation dramatically reduces the computational effort in the + * filter convolutions, without any loss of accuracy. + * The filter convolutions are also vectorizable on current hardware. + */ +class SincResampler final : public Resampler +{ +private: + /// Size of the ring buffer, must be a power of 2 + static const int RINGSIZE = 2048; + +private: + /// Table of the fir filter coefficients + matrix_t* firTable; + + int sampleIndex; + + /// Filter resolution + int firRES; + + /// Filter length + int firN; + + const int cyclesPerSample; + + int sampleOffset; + + int outputValue; + + short sample[RINGSIZE * 2]; + +private: + int fir(int subcycle); + +public: + /** + * Use a clock freqency of 985248Hz for PAL C64, 1022730Hz for NTSC C64. + * The default end of passband frequency is pass_freq = 0.9*sample_freq/2 + * for sample frequencies up to ~ 44.1kHz, and 20kHz for higher sample frequencies. + * + * For resampling, the ratio between the clock frequency and the sample frequency + * is limited as follows: 125*clock_freq/sample_freq < 16384 + * E.g. provided a clock frequency of ~ 1MHz, the sample frequency + * can not be set lower than ~ 8kHz. + * A lower sample frequency would make the resampling code overfill its 16k sample ring buffer. + * + * The end of passband frequency is also limited: pass_freq <= 0.9*sample_freq/2 + * + * E.g. for a 44.1kHz sampling rate the end of passband frequency is limited + * to slightly below 20kHz. This constraint ensures that the FIR table is not overfilled. + * + * @param clockFrequency System clock frequency at Hz + * @param samplingFrequency Desired output sampling rate + * @param highestAccurateFrequency + */ + SincResampler(double clockFrequency, double samplingFrequency, double highestAccurateFrequency); + + bool input(int input) override; + + int output() const override { return outputValue; } + + void reset() override; +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/resample/TwoPassSincResampler.h b/src/engine/platform/sound/c64_fp/resample/TwoPassSincResampler.h new file mode 100644 index 00000000..81659193 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/resample/TwoPassSincResampler.h @@ -0,0 +1,83 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2015 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef TWOPASSSINCRESAMPLER_H +#define TWOPASSSINCRESAMPLER_H + +#include + +#include + +#include "Resampler.h" +#include "SincResampler.h" + +#include "../sidcxx11.h" + +namespace reSIDfp +{ + +/** + * Compose a more efficient SINC from chaining two other SINCs. + */ +class TwoPassSincResampler final : public Resampler +{ +private: + std::unique_ptr const s1; + std::unique_ptr const s2; + +private: + TwoPassSincResampler(double clockFrequency, double samplingFrequency, double highestAccurateFrequency, double intermediateFrequency) : + s1(new SincResampler(clockFrequency, intermediateFrequency, highestAccurateFrequency)), + s2(new SincResampler(intermediateFrequency, samplingFrequency, highestAccurateFrequency)) + {} + +public: + // Named constructor + static TwoPassSincResampler* create(double clockFrequency, double samplingFrequency, double highestAccurateFrequency) + { + // Calculation according to Laurent Ganier. It evaluates to about 120 kHz at typical settings. + // Some testing around the chosen value seems to confirm that this does work. + double const intermediateFrequency = 2. * highestAccurateFrequency + + sqrt(2. * highestAccurateFrequency * clockFrequency + * (samplingFrequency - 2. * highestAccurateFrequency) / samplingFrequency); + return new TwoPassSincResampler(clockFrequency, samplingFrequency, highestAccurateFrequency, intermediateFrequency); + } + + bool input(int sample) override + { + return s1->input(sample) && s2->input(s1->output()); + } + + int output() const override + { + return s2->output(); + } + + void reset() override + { + s1->reset(); + s2->reset(); + } +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/resample/ZeroOrderResampler.h b/src/engine/platform/sound/c64_fp/resample/ZeroOrderResampler.h new file mode 100644 index 00000000..2bc80cde --- /dev/null +++ b/src/engine/platform/sound/c64_fp/resample/ZeroOrderResampler.h @@ -0,0 +1,88 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2011-2013 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef ZEROORDER_RESAMPLER_H +#define ZEROORDER_RESAMPLER_H + +#include "Resampler.h" + +#include "../sidcxx11.h" + +namespace reSIDfp +{ + +/** + * Return sample with linear interpolation. + * + * @author Antti Lankila + */ +class ZeroOrderResampler final : public Resampler +{ + +private: + /// Last sample + int cachedSample; + + /// Number of cycles per sample + const int cyclesPerSample; + + int sampleOffset; + + /// Calculated sample + int outputValue; + +public: + ZeroOrderResampler(double clockFrequency, double samplingFrequency) : + cachedSample(0), + cyclesPerSample(static_cast(clockFrequency / samplingFrequency * 1024.)), + sampleOffset(0), + outputValue(0) {} + + bool input(int sample) override + { + bool ready = false; + + if (sampleOffset < 1024) + { + outputValue = cachedSample + (sampleOffset * (sample - cachedSample) >> 10); + ready = true; + sampleOffset += cyclesPerSample; + } + + sampleOffset -= 1024; + + cachedSample = sample; + + return ready; + } + + int output() const override { return outputValue; } + + void reset() override + { + sampleOffset = 0; + cachedSample = 0; + } +}; + +} // namespace reSIDfp + +#endif diff --git a/src/engine/platform/sound/c64_fp/resample/test.cpp b/src/engine/platform/sound/c64_fp/resample/test.cpp new file mode 100644 index 00000000..5e5026ff --- /dev/null +++ b/src/engine/platform/sound/c64_fp/resample/test.cpp @@ -0,0 +1,87 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2012-2013 Leandro Nini + * Copyright 2007-2010 Antti Lankila + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include +#include +#include + +#include "siddefs-fp.h" + +#include "Resampler.h" +#include "TwoPassSincResampler.h" + +/** + * Simple sin waveform in, power output measurement function. + * It would be far better to use FFT. + */ +int main(int argc, const char* argv[]) +{ + const double RATE = 985248.4; + const int RINGSIZE = 2048; + + std::auto_ptr r(reSIDfp::TwoPassSincResampler::create(RATE, 48000.0, 20000.0)); + + std::map results; + clock_t start = clock(); + + for (double freq = 1000.; freq < RATE / 2.; freq *= 1.01) + { + /* prefill resampler buffer */ + int k = 0; + double omega = 2 * M_PI * freq / RATE; + + for (int j = 0; j < RINGSIZE; j ++) + { + int signal = static_cast(32768.0 * sin(k++ * omega) * sqrt(2)); + r->input(signal); + } + + int n = 0; + float pwr = 0; + + /* Now, during measurement stage, put 100 cycles of waveform through filter. */ + for (int j = 0; j < 100000; j ++) + { + int signal = static_cast(32768.0 * sin(k++ * omega) * sqrt(2)); + + if (r->input(signal)) + { + float out = r->output(); + pwr += out * out; + n += 1; + } + } + + results.insert(std::make_pair(freq, 10 * log10(pwr / n))); + } + + clock_t end = clock(); + + for (std::map::iterator it = results.begin(); it != results.end(); ++it) + { + std::cout << std::fixed << std::setprecision(0) << std::setw(6) << (*it).first << " Hz " << (*it).second << " dB" << std::endl; + } + + std::cout << "Filtering time " << (end - start) * 1000. / CLOCKS_PER_SEC << " ms" << std::endl; +} diff --git a/src/engine/platform/sound/c64_fp/sidcxx11.h b/src/engine/platform/sound/c64_fp/sidcxx11.h new file mode 100644 index 00000000..18eadf4a --- /dev/null +++ b/src/engine/platform/sound/c64_fp/sidcxx11.h @@ -0,0 +1,29 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2014-2015 Leandro Nini + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef SIDCXX11_H +#define SIDCXX11_H + +#define DEFAULT = default +#define DELETE = delete + +#define HAVE_CXX11 + +#endif diff --git a/src/engine/platform/sound/c64_fp/sidcxx14.h b/src/engine/platform/sound/c64_fp/sidcxx14.h new file mode 100644 index 00000000..5078a0b1 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/sidcxx14.h @@ -0,0 +1,29 @@ +/* + * This file is part of libsidplayfp, a SID player engine. + * + * Copyright 2014-2015 Leandro Nini + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef SIDCXX14_H +#define SIDCXX14_H + +#include "sidcxx11.h" + +#define MAKE_UNIQUE(type, ...) std::make_unique(__VA_ARGS__) +#define HAVE_CXX14 + +#endif diff --git a/src/engine/platform/sound/c64_fp/siddefs-fp.h b/src/engine/platform/sound/c64_fp/siddefs-fp.h new file mode 100644 index 00000000..43900862 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/siddefs-fp.h @@ -0,0 +1,62 @@ +// --------------------------------------------------------------------------- +// This file is part of reSID, a MOS6581 SID emulator engine. +// Copyright (C) 1999 Dag Lem +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// --------------------------------------------------------------------------- + +#ifndef SIDDEFS_FP_H +#define SIDDEFS_FP_H + +// Compilation configuration. +#define RESID_BRANCH_HINTS 0 + +// Compiler specifics. +#define HAVE_BUILTIN_EXPECT 0 + +#ifndef M_PI +# define M_PI 3.14159265358979323846 +#endif + +// Branch prediction macros, lifted off the Linux kernel. +#if RESID_BRANCH_HINTS && HAVE_BUILTIN_EXPECT +# define likely(x) __builtin_expect(!!(x), 1) +# define unlikely(x) __builtin_expect(!!(x), 0) +#else +# define likely(x) (x) +# define unlikely(x) (x) +#endif + +namespace reSIDfp { + +typedef enum { MOS6581=1, MOS8580 } ChipModel; + +typedef enum { DECIMATE=1, RESAMPLE } SamplingMethod; +} + +extern "C" +{ +#ifndef __VERSION_CC__ +extern const char* residfp_version_string; +#else +const char* residfp_version_string = "furnace"; +#endif +} + +// Inlining on/off. +#define RESID_INLINING 1 +#define RESID_INLINE inline + +#endif // SIDDEFS_FP_H diff --git a/src/engine/platform/sound/c64_fp/version.cc b/src/engine/platform/sound/c64_fp/version.cc new file mode 100644 index 00000000..3ed8b449 --- /dev/null +++ b/src/engine/platform/sound/c64_fp/version.cc @@ -0,0 +1,21 @@ +// --------------------------------------------------------------------------- +// This file is part of reSID, a MOS6581 SID emulator engine. +// Copyright (C) 2004 Dag Lem +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// --------------------------------------------------------------------------- + +#define __VERSION_CC__ +#include "siddefs-fp.h" diff --git a/src/gui/about.cpp b/src/gui/about.cpp index 65baf4fd..a79948b9 100644 --- a/src/gui/about.cpp +++ b/src/gui/about.cpp @@ -133,6 +133,8 @@ const char* aboutLine[]={ "puNES (NES, MMC5 and FDS) by FHorse", "NSFPlay (NES and FDS) by Brad Smith and Brezza", "reSID by Dag Lem", + "reSIDfp by Dag Lem, Antti Lankila", + "and Leandro Nini", "Stella by Stella Team", "QSound emulator by superctr and Valley Bell", "VICE VIC-20 sound core by Rami Rasanen and viznut", diff --git a/src/gui/gui.h b/src/gui/gui.h index 48b8e12c..2cd85020 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -1049,6 +1049,7 @@ class FurnaceGUI { int snCore; int nesCore; int fdsCore; + int c64Core; int pcSpeakerOutMethod; String yrw801Path; String tg100Path; @@ -1168,6 +1169,7 @@ class FurnaceGUI { snCore(0), nesCore(0), fdsCore(0), + c64Core(1), pcSpeakerOutMethod(0), yrw801Path(""), tg100Path(""), diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index 1e9a1445..27720112 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -97,6 +97,11 @@ const char* nesCores[]={ "NSFplay" }; +const char* c64Cores[]={ + "reSID", + "reSIDfp" +}; + const char* pcspkrOutMethods[]={ "evdev SND_TONE", "KIOCSOUND on /dev/tty1", @@ -972,6 +977,10 @@ void FurnaceGUI::drawSettings() { ImGui::SameLine(); ImGui::Combo("##FDSCore",&settings.fdsCore,nesCores,2); + ImGui::Text("SID core"); + ImGui::SameLine(); + ImGui::Combo("##C64Core",&settings.c64Core,c64Cores,2); + ImGui::Separator(); ImGui::Text("PC Speaker strategy"); @@ -2155,6 +2164,7 @@ void FurnaceGUI::syncSettings() { settings.snCore=e->getConfInt("snCore",0); settings.nesCore=e->getConfInt("nesCore",0); settings.fdsCore=e->getConfInt("fdsCore",0); + settings.c64Core=e->getConfInt("c64Core",1); settings.pcSpeakerOutMethod=e->getConfInt("pcSpeakerOutMethod",0); settings.yrw801Path=e->getConfString("yrw801Path",""); settings.tg100Path=e->getConfString("tg100Path",""); @@ -2267,6 +2277,7 @@ void FurnaceGUI::syncSettings() { clampSetting(settings.snCore,0,1); clampSetting(settings.nesCore,0,1); clampSetting(settings.fdsCore,0,1); + clampSetting(settings.c64Core,0,1); clampSetting(settings.pcSpeakerOutMethod,0,4); clampSetting(settings.mainFont,0,6); clampSetting(settings.patFont,0,6); @@ -2402,6 +2413,7 @@ void FurnaceGUI::commitSettings() { e->setConf("snCore",settings.snCore); e->setConf("nesCore",settings.nesCore); e->setConf("fdsCore",settings.fdsCore); + e->setConf("c64Core",settings.c64Core); e->setConf("pcSpeakerOutMethod",settings.pcSpeakerOutMethod); e->setConf("yrw801Path",settings.yrw801Path); e->setConf("tg100Path",settings.tg100Path); diff --git a/src/main.cpp b/src/main.cpp index 39c9e6ca..a1856092 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -181,6 +181,7 @@ TAParamResult pVersion(String) { printf("- puNES by FHorse (GPLv2)\n"); printf("- NSFPlay by Brad Smith and Brezza (unknown open-source license)\n"); printf("- reSID by Dag Lem (GPLv2)\n"); + printf("- reSIDfp by Dag Lem, Antti Lankila and Leandro Nini (GPLv2)\n"); printf("- Stella by Stella Team (GPLv2)\n"); printf("- vgsound_emu (first version) by cam900 (BSD 3-clause)\n"); return TA_PARAM_QUIT;