1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-11-25 16:31:42 +00:00

Merge pull request #783 from TomHarte/OPL2

Adds provisional OPLL emulation.
This commit is contained in:
Thomas Harte 2020-05-09 18:28:03 -04:00 committed by GitHub
commit 5c1ae40a9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1951 additions and 67 deletions

View File

@ -234,7 +234,7 @@ template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() {
}
}
template <bool is_stereo> bool AY38910<is_stereo>::is_zero_level() {
template <bool is_stereo> bool AY38910<is_stereo>::is_zero_level() const {
// Confirm that the AY is trivially at the zero level if all three volume controls are set to fixed zero.
return output_registers_[0x8] == 0 && output_registers_[0x9] == 0 && output_registers_[0xa] == 0;
}

View File

@ -107,7 +107,7 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter.
void get_samples(std::size_t number_of_samples, int16_t *target);
bool is_zero_level();
bool is_zero_level() const;
void set_sample_volume_range(std::int16_t range);
static constexpr bool get_is_stereo() { return is_stereo; }

View File

@ -15,7 +15,7 @@ using namespace Konami;
SCC::SCC(Concurrency::DeferringAsyncTaskQueue &task_queue) :
task_queue_(task_queue) {}
bool SCC::is_zero_level() {
bool SCC::is_zero_level() const {
return !(channel_enable_ & 0x1f);
}

View File

@ -27,7 +27,7 @@ class SCC: public ::Outputs::Speaker::SampleSource {
SCC(Concurrency::DeferringAsyncTaskQueue &task_queue);
/// As per ::SampleSource; provides a broadphase test for silence.
bool is_zero_level();
bool is_zero_level() const;
/// As per ::SampleSource; provides audio output.
void get_samples(std::size_t number_of_samples, std::int16_t *target);

View File

@ -0,0 +1,263 @@
//
// EnvelopeGenerator.hpp
// Clock Signal
//
// Created by Thomas Harte on 01/05/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef EnvelopeGenerator_h
#define EnvelopeGenerator_h
#include <optional>
#include <functional>
#include "LowFrequencyOscillator.hpp"
namespace Yamaha {
namespace OPL {
/*!
Models an OPL-style envelope generator.
Damping is optional; if damping is enabled then if there is a transition to key-on while
attenuation is less than maximum then attenuation will be quickly transitioned to maximum
before the attack phase can begin.
in real hardware damping is used by the envelope generators associated with
carriers, with phases being reset upon the transition from damping to attack.
This code considers application of tremolo to be a function of the envelope generator;
this is largely for logical conformity with the phase generator that necessarily has to
apply vibrato.
TODO: use envelope_precision.
*/
template <int envelope_precision, int period_precision> class EnvelopeGenerator {
public:
/*!
Advances the envelope generator a single step, given the current state of the low-frequency oscillator, @c oscillator.
*/
void update(const LowFrequencyOscillator &oscillator) {
// Apply tremolo, which is fairly easy.
tremolo_ = tremolo_enable_ * oscillator.tremolo << 4;
// Something something something...
const int key_scaling_rate = key_scale_rate_ >> key_scale_rate_shift_;
switch(phase_) {
case Phase::Damp:
update_decay(oscillator, 12 << 2);
if(attenuation_ == 511) {
(*will_attack_)();
phase_ = Phase::Attack;
}
break;
case Phase::Attack:
update_attack(oscillator, attack_rate_ + key_scaling_rate);
// Two possible terminating conditions: (i) the attack rate is 15; (ii) full volume has been reached.
if(attenuation_ <= 0) {
attenuation_ = 0;
phase_ = Phase::Decay;
}
break;
case Phase::Decay:
update_decay(oscillator, decay_rate_ + key_scaling_rate);
if(attenuation_ >= sustain_level_) {
attenuation_ = sustain_level_;
phase_ = use_sustain_level_ ? Phase::Sustain : Phase::Release;
}
break;
case Phase::Sustain:
// Nothing to do.
break;
case Phase::Release:
update_decay(oscillator, release_rate_ + key_scaling_rate);
break;
}
}
/*!
@returns The current attenuation from this envelope generator. This is independent of the envelope precision.
*/
int attenuation() const {
return (attenuation_ + tremolo_) << 3;
}
/*!
Enables or disables damping on this envelope generator. If damping is enabled then this envelope generator will
use the damping phase when necessary (i.e. when transitioning to key on if attenuation is not already at maximum)
and in any case will call @c will_attack before transitioning from any other state to attack.
@param will_attack Supply a will_attack callback to enable damping mode; supply nullopt to disable damping mode.
*/
void set_should_damp(const std::optional<std::function<void(void)>> &will_attack) {
will_attack_ = will_attack;
}
/*!
Sets the current state of the key-on input.
*/
void set_key_on(bool key_on) {
// Do nothing if this is not a leading or trailing edge.
if(key_on == key_on_) return;
key_on_ = key_on;
// Always transition to release upon a key off.
if(!key_on_) {
phase_ = Phase::Release;
return;
}
// On key on: if this is an envelope generator with damping, and damping is required,
// schedule that. If damping is not required, announce a pending attack now and
// transition to attack.
if(will_attack_) {
if(attenuation_ != 511) {
phase_ = Phase::Damp;
return;
}
(*will_attack_)();
}
phase_ = Phase::Attack;
}
/*!
Sets the attack rate, which should be in the range 015.
*/
void set_attack_rate(int rate) {
attack_rate_ = rate << 2;
}
/*!
Sets the decay rate, which should be in the range 015.
*/
void set_decay_rate(int rate) {
decay_rate_ = rate << 2;
}
/*!
Sets the release rate, which should be in the range 015.
*/
void set_release_rate(int rate) {
release_rate_ = rate << 2;
}
/*!
Sets the sustain level, which should be in the range 015.
*/
void set_sustain_level(int level) {
sustain_level_ = level << 3;
// TODO: verify the shift level here. Especially re: precision.
}
/*!
Enables or disables use of the sustain level. If this is disabled, the envelope proceeds
directly from decay to release.
*/
void set_use_sustain_level(bool use) {
use_sustain_level_ = use;
}
/*!
Enables or disables key-rate scaling.
*/
void set_key_scaling_rate_enabled(bool enabled) {
key_scale_rate_shift_ = int(enabled) * 2;
}
/*!
Enables or disables application of the low-frequency oscillator's tremolo.
*/
void set_tremolo_enabled(bool enabled) {
tremolo_enable_ = int(enabled);
}
/*!
Sets the current period associated with the channel that owns this envelope generator;
this is used to select a key scaling rate if key-rate scaling is enabled.
*/
void set_period(int period, int octave) {
key_scale_rate_ = (octave << 1) | (period >> (period_precision - 1));
}
private:
enum class Phase {
Attack, Decay, Sustain, Release, Damp
} phase_ = Phase::Release;
int attenuation_ = 511, tremolo_ = 0;
bool key_on_ = false;
std::optional<std::function<void(void)>> will_attack_;
int key_scale_rate_ = 0;
int key_scale_rate_shift_ = 0;
int tremolo_enable_ = 0;
int attack_rate_ = 0;
int decay_rate_ = 0;
int release_rate_ = 0;
int sustain_level_ = 0;
bool use_sustain_level_ = false;
static constexpr int dithering_patterns[4][8] = {
{0, 1, 0, 1, 0, 1, 0, 1},
{0, 1, 0, 1, 1, 1, 0, 1},
{0, 1, 1, 1, 0, 1, 1, 1},
{0, 1, 1, 1, 1, 1, 1, 1},
};
void update_attack(const LowFrequencyOscillator &oscillator, int rate) {
// Special case: no attack.
if(rate < 4) {
return;
}
// Special case: instant attack.
if(rate >= 60) {
attenuation_ = 0;
return;
}
// Work out the number of cycles between each adjustment tick, and stop now
// if not at the next adjustment boundary.
const int shift_size = 13 - (std::min(rate, 52) >> 2);
if(oscillator.counter & ((1 << shift_size) - 1)) {
return;
}
// Apply dithered adjustment.
const int rate_shift = (rate > 55);
const int step = dithering_patterns[rate & 3][(oscillator.counter >> shift_size) & 7];
attenuation_ -= ((attenuation_ >> (3 - rate_shift)) + 1) * step;
}
void update_decay(const LowFrequencyOscillator &oscillator, int rate) {
// Special case: no decay.
if(rate < 4) {
return;
}
// Work out the number of cycles between each adjustment tick, and stop now
// if not at the next adjustment boundary.
const int shift_size = 13 - (std::min(rate, 52) >> 2);
if(oscillator.counter & ((1 << shift_size) - 1)) {
return;
}
// Apply dithered adjustment and clamp.
const int rate_shift = 1 + (rate > 59) + (rate > 55);
attenuation_ += dithering_patterns[rate & 3][(oscillator.counter >> shift_size) & 7] * (4 << rate_shift);
attenuation_ = std::min(attenuation_, 511);
}
};
}
}
#endif /* EnvelopeGenerator_h */

View File

@ -0,0 +1,58 @@
//
// KeyLevelScaler.hpp
// Clock Signal
//
// Created by Thomas Harte on 02/05/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef KeyLevelScaler_h
#define KeyLevelScaler_h
namespace Yamaha {
namespace OPL {
template <int frequency_precision> class KeyLevelScaler {
public:
/*!
Sets the current period associated with the channel that owns this envelope generator;
this is used to select a key scaling rate if key-rate scaling is enabled.
*/
void set_period(int period, int octave) {
constexpr int key_level_scales[16] = {0, 48, 64, 74, 80, 86, 90, 94, 96, 100, 102, 104, 106, 108, 110, 112};
constexpr int masks[2] = {~0, 0};
// A two's complement assumption is embedded below; the use of masks relies
// on the sign bit to clamp to zero.
level_ = key_level_scales[period >> (frequency_precision - 4)];
level_ -= 16 * (octave ^ 7);
level_ &= masks[(level_ >> ((sizeof(int) * 8) - 1)) & 1];
}
/*!
Enables or disables key-rate scaling.
*/
void set_key_scaling_level(int level) {
// '7' is just a number large enough to render all possible scaling coefficients as 0.
constexpr int key_level_scale_shifts[4] = {7, 1, 2, 0};
shift_ = key_level_scale_shifts[level];
}
/*!
@returns The current attenuation level due to key-level scaling.
*/
int attenuation() const {
return level_ >> shift_;
}
private:
int level_ = 0;
int shift_ = 0;
};
}
}
#endif /* KeyLevelScaler_h */

View File

@ -0,0 +1,68 @@
//
// LowFrequencyOscillator.hpp
// Clock Signal
//
// Created by Thomas Harte on 23/04/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef LowFrequencyOscillator_hpp
#define LowFrequencyOscillator_hpp
#include "../../../Numeric/LFSR.hpp"
namespace Yamaha {
namespace OPL {
/*!
Models the output of the OPL low-frequency oscillator, which provides a couple of optional fixed-frequency
modifications to an operator: tremolo and vibrato. Also exposes a global time counter, which oscillators use
as part of their ADSR envelope.
*/
class LowFrequencyOscillator {
public:
/// Current attenuation due to tremolo / amplitude modulation, between 0 and 26.
int tremolo = 0;
/// A number between 0 and 7 indicating the current vibrato offset; this should be combined by operators
/// with their frequency number to get the actual vibrato.
int vibrato = 0;
/// A counter of the number of operator update cycles (i.e. input clock / 72) since an arbitrary time.
int counter = 0;
/// Describes the current output of the LFSR; will be either 0 or 1.
int lfsr = 0;
/// Updates the oscillator outputs. Should be called at the (input clock/72) rate.
void update() {
++counter;
// This produces output of:
//
// four instances of 0, four instances of 1... _three_ instances of 26,
// four instances of 25, four instances of 24... _three_ instances of 0.
//
// ... advancing once every 64th update.
const int tremolo_index = (counter >> 6) % 210;
const int tremolo_levels[2] = {tremolo_index >> 2, 52 - ((tremolo_index+1) >> 2)};
tremolo = tremolo_levels[tremolo_index / 107];
// Vibrato is relatively simple: it's just three bits from the counter.
vibrato = (counter >> 10) & 7;
}
/// Updartes the LFSR output. Should be called at the input clock rate.
void update_lfsr() {
lfsr = noise_source_.next();
}
private:
// This is the correct LSFR per forums.submarine.org.uk.
Numeric::LFSR<int, 0x800302> noise_source_;
};
}
}
#endif /* LowFrequencyOscillator_hpp */

View File

@ -0,0 +1,40 @@
//
// OPLBase.hpp
// Clock Signal
//
// Created by Thomas Harte on 03/05/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef OPLBase_h
#define OPLBase_h
#include "../../../Outputs/Speaker/Implementation/SampleSource.hpp"
#include "../../../Concurrency/AsyncTaskQueue.hpp"
namespace Yamaha {
namespace OPL {
template <typename Child> class OPLBase: public ::Outputs::Speaker::SampleSource {
public:
void write(uint16_t address, uint8_t value) {
if(address & 1) {
static_cast<Child *>(this)->write_register(selected_register_, value);
} else {
selected_register_ = value;
}
}
protected:
OPLBase(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {}
Concurrency::DeferringAsyncTaskQueue &task_queue_;
private:
uint8_t selected_register_ = 0;
};
}
}
#endif /* OPLBase_h */

View File

@ -0,0 +1,125 @@
//
// PhaseGenerator.h
// Clock Signal
//
// Created by Thomas Harte on 30/04/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef PhaseGenerator_h
#define PhaseGenerator_h
#include <cassert>
#include "LowFrequencyOscillator.hpp"
#include "Tables.hpp"
namespace Yamaha {
namespace OPL {
/*!
Models an OPL-style phase generator of templated precision; having been told its period ('f-num'), octave ('block') and
multiple, and whether to apply vibrato, this will then appropriately update and return phase.
*/
template <int precision> class PhaseGenerator {
public:
/*!
Advances the phase generator a single step, given the current state of the low-frequency oscillator, @c oscillator.
*/
void update(const LowFrequencyOscillator &oscillator) {
constexpr int vibrato_shifts[8] = {3, 1, 0, 1, 3, 1, 0, 1};
constexpr int vibrato_signs[2] = {1, -1};
// Get just the top three bits of the period_.
const int top_freq = period_ >> (precision - 3);
// Cacluaute applicable vibrato as a function of (i) the top three bits of the
// oscillator period; (ii) the current low-frequency oscillator vibrato output; and
// (iii) whether vibrato is enabled.
const int vibrato = (top_freq >> vibrato_shifts[oscillator.vibrato]) * vibrato_signs[oscillator.vibrato >> 2] * enable_vibrato_;
// Apply phase update with vibrato from the low-frequency oscillator.
phase_ += multiple_ * (period_ + vibrato) << octave_;
}
/*!
@returns Current phase; real hardware provides only the low ten bits of this result.
*/
int phase() const {
// My table if multipliers is multiplied by two, so shift by one more
// than the stated precision.
return phase_ >> precision_shift;
}
/*!
@returns Current phase, scaled up by (1 << precision).
*/
int scaled_phase() const {
return phase_ >> 1;
}
/*!
Applies feedback based on two historic samples of a total output level,
plus the degree of feedback to apply
*/
void apply_feedback(LogSign first, LogSign second, int level) {
constexpr int masks[] = {0, ~0, ~0, ~0, ~0, ~0, ~0, ~0};
phase_ += ((second.level(precision) + first.level(precision)) >> (8 - level)) & masks[level];
}
/*!
Sets the multiple for this phase generator, in the same terms as an OPL programmer,
i.e. a 4-bit number that is used as a lookup into the internal multiples table.
*/
void set_multiple(int multiple) {
// This encodes the MUL -> multiple table given on page 12,
// multiplied by two.
constexpr int multipliers[] = {
1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 20, 24, 24, 30, 30
};
assert(multiple < 16);
multiple_ = multipliers[multiple];
}
/*!
Sets the period of this generator, along with its current octave.
Yamaha tends to refer to the period as the 'f-number', and used both 'octave' and 'block' for octave.
*/
void set_period(int period, int octave) {
period_ = period;
octave_ = octave;
assert(octave_ < 8);
assert(period_ < (1 << precision));
}
/*!
Enables or disables vibrato.
*/
void set_vibrato_enabled(bool enabled) {
enable_vibrato_ = int(enabled);
}
/*!
Resets the current phase.
*/
void reset() {
phase_ = 0;
}
private:
static constexpr int precision_shift = 1 + precision;
int phase_ = 0;
int multiple_ = 0;
int period_ = 0;
int octave_ = 0;
int enable_vibrato_ = 0;
};
}
}
#endif /* PhaseGenerator_h */

View File

@ -0,0 +1,227 @@
//
// Tables.hpp
// Clock Signal
//
// Created by Thomas Harte on 15/04/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef Tables_hpp
#define Tables_hpp
namespace Yamaha {
namespace OPL {
/*
These are the OPL's built-in log-sin and exponentiation tables, as recovered by
Matthew Gambrell and Olli Niemitalo in 'OPLx decapsulated'. Despite the formulas
being well known, I've elected not to generate these at runtime because even if I
did, I'd just end up with the proper values laid out in full in a unit test, and
they're very compact.
*/
/*!
Represents both the logarithm of a value and its sign.
It's actually the negative logarithm, in base two, in fixed point.
*/
struct LogSign {
int log;
int sign;
void reset() {
log = 0;
sign = 1;
}
LogSign &operator +=(int attenuation) {
log += attenuation;
return *this;
}
LogSign &operator +=(LogSign log_sign) {
log += log_sign.log;
sign *= log_sign.sign;
return *this;
}
int level(int fractional = 0) const;
};
/*!
@returns Negative log sin of x, assuming a 1024-unit circle.
*/
constexpr LogSign negative_log_sin(int x) {
/// Defines the first quadrant of 1024-unit negative log to the base two of sine (that conveniently misses sin(0)).
///
/// Expected branchless usage for a full 1024 unit output:
///
/// constexpr int multiplier[] = { 1, -1 };
/// constexpr int mask[] = { 0, 255 };
///
/// value = exp( log_sin[angle & 255] ^ mask[(angle >> 8) & 1]) * multitplier[(angle >> 9) & 1]
///
/// ... where exp(x) = 2 ^ -x / 256
constexpr int16_t log_sin[] = {
2137, 1731, 1543, 1419, 1326, 1252, 1190, 1137,
1091, 1050, 1013, 979, 949, 920, 894, 869,
846, 825, 804, 785, 767, 749, 732, 717,
701, 687, 672, 659, 646, 633, 621, 609,
598, 587, 576, 566, 556, 546, 536, 527,
518, 509, 501, 492, 484, 476, 468, 461,
453, 446, 439, 432, 425, 418, 411, 405,
399, 392, 386, 380, 375, 369, 363, 358,
352, 347, 341, 336, 331, 326, 321, 316,
311, 307, 302, 297, 293, 289, 284, 280,
276, 271, 267, 263, 259, 255, 251, 248,
244, 240, 236, 233, 229, 226, 222, 219,
215, 212, 209, 205, 202, 199, 196, 193,
190, 187, 184, 181, 178, 175, 172, 169,
167, 164, 161, 159, 156, 153, 151, 148,
146, 143, 141, 138, 136, 134, 131, 129,
127, 125, 122, 120, 118, 116, 114, 112,
110, 108, 106, 104, 102, 100, 98, 96,
94, 92, 91, 89, 87, 85, 83, 82,
80, 78, 77, 75, 74, 72, 70, 69,
67, 66, 64, 63, 62, 60, 59, 57,
56, 55, 53, 52, 51, 49, 48, 47,
46, 45, 43, 42, 41, 40, 39, 38,
37, 36, 35, 34, 33, 32, 31, 30,
29, 28, 27, 26, 25, 24, 23, 23,
22, 21, 20, 20, 19, 18, 17, 17,
16, 15, 15, 14, 13, 13, 12, 12,
11, 10, 10, 9, 9, 8, 8, 7,
7, 7, 6, 6, 5, 5, 5, 4,
4, 4, 3, 3, 3, 2, 2, 2,
2, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0
};
constexpr int16_t sign[] = { 1, -1 };
constexpr int16_t mask[] = { 0, 255 };
return {
.log = log_sin[(x & 255) ^ mask[(x >> 8) & 1]],
.sign = sign[(x >> 9) & 1]
};
}
/*!
Computes the linear value represented by the log-sign @c ls, shifted left by @c fractional prior
to loss of precision.
*/
constexpr int power_two(LogSign ls, int fractional = 0) {
/// A derivative of the exponent table in a real OPL2; mapped_exp[x] = (source[c ^ 0xff] << 1) | 0x800.
///
/// The ahead-of-time transformation represents fixed work the OPL2 does when reading its table
/// independent on the input.
///
/// The original table is a 0.10 fixed-point representation of 2^x - 1 with bit 10 implicitly set, where x is
/// in 0.8 fixed point.
///
/// Since the log_sin table represents sine in a negative base-2 logarithm, values from it would need
/// to be negatived before being put into the original table. That's haned with the ^ 0xff. The | 0x800 is to
/// set the implicit bit 10 (subject to the shift).
///
/// The shift by 1 is to allow the chip's exploitation of the recursive symmetry of the exponential table to
/// be achieved more easily. Specifically, to convert a logarithmic attenuation to a linear one, just perform:
///
/// result = mapped_exp[x & 0xff] >> (x >> 8)
constexpr int16_t mapped_exp[] = {
4084, 4074, 4062, 4052, 4040, 4030, 4020, 4008,
3998, 3986, 3976, 3966, 3954, 3944, 3932, 3922,
3912, 3902, 3890, 3880, 3870, 3860, 3848, 3838,
3828, 3818, 3808, 3796, 3786, 3776, 3766, 3756,
3746, 3736, 3726, 3716, 3706, 3696, 3686, 3676,
3666, 3656, 3646, 3636, 3626, 3616, 3606, 3596,
3588, 3578, 3568, 3558, 3548, 3538, 3530, 3520,
3510, 3500, 3492, 3482, 3472, 3464, 3454, 3444,
3434, 3426, 3416, 3408, 3398, 3388, 3380, 3370,
3362, 3352, 3344, 3334, 3326, 3316, 3308, 3298,
3290, 3280, 3272, 3262, 3254, 3246, 3236, 3228,
3218, 3210, 3202, 3192, 3184, 3176, 3168, 3158,
3150, 3142, 3132, 3124, 3116, 3108, 3100, 3090,
3082, 3074, 3066, 3058, 3050, 3040, 3032, 3024,
3016, 3008, 3000, 2992, 2984, 2976, 2968, 2960,
2952, 2944, 2936, 2928, 2920, 2912, 2904, 2896,
2888, 2880, 2872, 2866, 2858, 2850, 2842, 2834,
2826, 2818, 2812, 2804, 2796, 2788, 2782, 2774,
2766, 2758, 2752, 2744, 2736, 2728, 2722, 2714,
2706, 2700, 2692, 2684, 2678, 2670, 2664, 2656,
2648, 2642, 2634, 2628, 2620, 2614, 2606, 2600,
2592, 2584, 2578, 2572, 2564, 2558, 2550, 2544,
2536, 2530, 2522, 2516, 2510, 2502, 2496, 2488,
2482, 2476, 2468, 2462, 2456, 2448, 2442, 2436,
2428, 2422, 2416, 2410, 2402, 2396, 2390, 2384,
2376, 2370, 2364, 2358, 2352, 2344, 2338, 2332,
2326, 2320, 2314, 2308, 2300, 2294, 2288, 2282,
2276, 2270, 2264, 2258, 2252, 2246, 2240, 2234,
2228, 2222, 2216, 2210, 2204, 2198, 2192, 2186,
2180, 2174, 2168, 2162, 2156, 2150, 2144, 2138,
2132, 2128, 2122, 2116, 2110, 2104, 2098, 2092,
2088, 2082, 2076, 2070, 2064, 2060, 2054, 2048,
};
return ((mapped_exp[ls.log & 0xff] << fractional) >> (ls.log >> 8)) * ls.sign;
}
/*
Credit for the fixed register lists goes to Nuke.YKT; I found them at:
https://siliconpr0n.org/archive/doku.php?id=vendor:yamaha:opl2#ym2413_instrument_rom
The arrays below begin with channel 1, then each line is a single channel defined
in exactly the same terms as the OPL's user-defined channel.
*/
constexpr uint8_t opll_patch_set[] = {
0x71, 0x61, 0x1e, 0x17, 0xd0, 0x78, 0x00, 0x17,
0x13, 0x41, 0x1a, 0x0d, 0xd8, 0xf7, 0x23, 0x13,
0x13, 0x01, 0x99, 0x00, 0xf2, 0xc4, 0x11, 0x23,
0x31, 0x61, 0x0e, 0x07, 0xa8, 0x64, 0x70, 0x27,
0x32, 0x21, 0x1e, 0x06, 0xe0, 0x76, 0x00, 0x28,
0x31, 0x22, 0x16, 0x05, 0xe0, 0x71, 0x00, 0x18,
0x21, 0x61, 0x1d, 0x07, 0x82, 0x81, 0x10, 0x07,
0x23, 0x21, 0x2d, 0x14, 0xa2, 0x72, 0x00, 0x07,
0x61, 0x61, 0x1b, 0x06, 0x64, 0x65, 0x10, 0x17,
0x41, 0x61, 0x0b, 0x18, 0x85, 0xf7, 0x71, 0x07,
0x13, 0x01, 0x83, 0x11, 0xfa, 0xe4, 0x10, 0x04,
0x17, 0xc1, 0x24, 0x07, 0xf8, 0xf8, 0x22, 0x12,
0x61, 0x50, 0x0c, 0x05, 0xc2, 0xf5, 0x20, 0x42,
0x01, 0x01, 0x55, 0x03, 0xc9, 0x95, 0x03, 0x02,
0x61, 0x41, 0x89, 0x03, 0xf1, 0xe4, 0x40, 0x13,
};
constexpr uint8_t vrc7_patch_set[] = {
0x03, 0x21, 0x05, 0x06, 0xe8, 0x81, 0x42, 0x27,
0x13, 0x41, 0x14, 0x0d, 0xd8, 0xf6, 0x23, 0x12,
0x11, 0x11, 0x08, 0x08, 0xfa, 0xb2, 0x20, 0x12,
0x31, 0x61, 0x0c, 0x07, 0xa8, 0x64, 0x61, 0x27,
0x32, 0x21, 0x1e, 0x06, 0xe1, 0x76, 0x01, 0x28,
0x02, 0x01, 0x06, 0x00, 0xa3, 0xe2, 0xf4, 0xf4,
0x21, 0x61, 0x1d, 0x07, 0x82, 0x81, 0x11, 0x07,
0x23, 0x21, 0x22, 0x17, 0xa2, 0x72, 0x01, 0x17,
0x35, 0x11, 0x25, 0x00, 0x40, 0x73, 0x72, 0x01,
0xb5, 0x01, 0x0f, 0x0f, 0xa8, 0xa5, 0x51, 0x02,
0x17, 0xc1, 0x24, 0x07, 0xf8, 0xf8, 0x22, 0x12,
0x71, 0x23, 0x11, 0x06, 0x65, 0x74, 0x18, 0x16,
0x01, 0x02, 0xd3, 0x05, 0xc9, 0x95, 0x03, 0x02,
0x61, 0x63, 0x0c, 0x00, 0x94, 0xc0, 0x33, 0xf6,
0x21, 0x72, 0x0d, 0x00, 0xc1, 0xd5, 0x56, 0x06,
};
constexpr uint8_t percussion_patch_set[] = {
0x01, 0x01, 0x18, 0x0f, 0xdf, 0xf8, 0x6a, 0x6d,
0x01, 0x01, 0x00, 0x00, 0xc8, 0xd8, 0xa7, 0x48,
0x05, 0x01, 0x00, 0x00, 0xf8, 0xaa, 0x59, 0x55,
};
inline int LogSign::level(int fractional) const {
return power_two(*this, fractional);
}
}
}
#endif /* Tables_hpp */

View File

@ -0,0 +1,92 @@
//
// WaveformGenerator.hpp
// Clock Signal
//
// Created by Thomas Harte on 03/05/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef WaveformGenerator_h
#define WaveformGenerator_h
#include "Tables.hpp"
#include "LowFrequencyOscillator.hpp"
namespace Yamaha {
namespace OPL {
enum class Waveform {
Sine, HalfSine, AbsSine, PulseSine
};
template <int phase_precision> class WaveformGenerator {
public:
/*!
@returns The output of waveform @c form at [integral] phase @c phase.
*/
static constexpr LogSign wave(Waveform form, int phase) {
constexpr int waveforms[4][4] = {
{1023, 1023, 1023, 1023}, // Sine: don't mask in any quadrant.
{511, 511, 0, 0}, // Half sine: keep the first half intact, lock to 0 in the second half.
{511, 511, 511, 511}, // AbsSine: endlessly repeat the first half of the sine wave.
{255, 0, 255, 0}, // PulseSine: act as if the first quadrant is in the first and third; lock the other two to 0.
};
return negative_log_sin(phase & waveforms[int(form)][(phase >> 8) & 3]);
}
/*!
@returns The output of waveform @c form at [scaled] phase @c scaled_phase given the modulation input @c modulation.
*/
static constexpr LogSign wave(Waveform form, int scaled_phase, LogSign modulation) {
const int scaled_phase_offset = modulation.level(phase_precision);
const int phase = (scaled_phase + scaled_phase_offset) >> phase_precision;
return wave(form, phase);
}
/*!
@returns Snare output, calculated from the current LFSR state as captured in @c oscillator and an operator's phase.
*/
static constexpr LogSign snare(const LowFrequencyOscillator &oscillator, int phase) {
// If noise is 0, output is positive.
// If noise is 1, output is negative.
// If (noise ^ sign) is 0, output is 0. Otherwise it is max.
const int sign = phase & 0x200;
const int level = ((phase >> 9) & 1) ^ oscillator.lfsr;
return negative_log_sin(sign + (level << 8));
}
/*!
@returns Cymbal output, calculated from an operator's phase and a modulator's phase.
*/
static constexpr LogSign cymbal(int carrier_phase, int modulator_phase) {
return negative_log_sin(256 + (phase_combination(carrier_phase, modulator_phase) << 9));
}
/*!
@returns High-hat output, calculated from the current LFSR state as captured in @c oscillator, an operator's phase and a modulator's phase.
*/
static constexpr LogSign high_hat(const LowFrequencyOscillator &oscillator, int carrier_phase, int modulator_phase) {
constexpr int angles[] = {0x234, 0xd0, 0x2d0, 0x34};
return negative_log_sin(angles[
phase_combination(carrier_phase, modulator_phase) |
(oscillator.lfsr << 1)
]);
}
private:
/*!
@returns The phase bit used for cymbal and high-hat generation, which is a function of two operators' phases.
*/
static constexpr int phase_combination(int carrier_phase, int modulator_phase) {
return (
((carrier_phase >> 5) ^ (carrier_phase >> 3)) &
((modulator_phase >> 7) ^ (modulator_phase >> 2)) &
((carrier_phase >> 5) ^ (modulator_phase >> 3))
) & 1;
}
};
}
}
#endif /* WaveformGenerator_h */

431
Components/OPx/OPLL.cpp Normal file
View File

@ -0,0 +1,431 @@
//
// OPLL.cpp
// Clock Signal
//
// Created by Thomas Harte on 03/05/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#include "OPLL.hpp"
#include <cassert>
using namespace Yamaha::OPL;
OPLL::OPLL(Concurrency::DeferringAsyncTaskQueue &task_queue, int audio_divider, bool is_vrc7):
OPLBase(task_queue), audio_divider_(audio_divider), is_vrc7_(is_vrc7) {
// Due to the way that sound mixing works on the OPLL, the audio divider may not
// be larger than 4.
assert(audio_divider <= 4);
// Setup the rhythm envelope generators.
// Treat the bass exactly as if it were a melodic channel.
rhythm_envelope_generators_[BassCarrier].set_should_damp([this] {
// Propagate attack mode to the modulator, and reset both phases.
rhythm_envelope_generators_[BassModulator].set_key_on(true);
phase_generators_[6 + 0].reset();
phase_generators_[6 + 9].reset();
});
// Crib the proper rhythm envelope generator settings by installing
// the rhythm instruments and copying them over.
rhythm_mode_enabled_ = true;
install_instrument(6);
install_instrument(7);
install_instrument(8);
rhythm_envelope_generators_[BassCarrier] = envelope_generators_[6];
rhythm_envelope_generators_[BassModulator] = envelope_generators_[6 + 9];
rhythm_envelope_generators_[HighHat] = envelope_generators_[7 + 9];
rhythm_envelope_generators_[Cymbal] = envelope_generators_[8];
rhythm_envelope_generators_[TomTom] = envelope_generators_[8 + 9];
rhythm_envelope_generators_[Snare] = envelope_generators_[7];
// Return to ordinary default mode.
rhythm_mode_enabled_ = false;
// Set up proper damping management.
for(int c = 0; c < 9; ++c) {
envelope_generators_[c].set_should_damp([this, c] {
// Propagate attack mode to the modulator, and reset both phases.
envelope_generators_[c + 9].set_key_on(true);
phase_generators_[c + 0].reset();
phase_generators_[c + 9].reset();
});
}
// Set default instrument.
for(int c = 0; c < 9; ++c) {
install_instrument(c);
}
}
// MARK: - Machine-facing programmatic input.
void OPLL::write_register(uint8_t address, uint8_t value) {
// The OPLL doesn't have timers or other non-audio functions, so all writes
// go to the audio queue.
task_queue_.defer([this, address, value] {
// The first 8 locations are used to define the custom instrument, and have
// exactly the same format as the patch set arrays at the head of this file.
if(address < 8) {
custom_instrument_[address] = value;
// Update all channels that refer to instrument 0.
for(int c = 0; c < 9; ++c) {
if(!channels_[c].instrument) {
install_instrument(c);
}
}
return;
}
// Register 0xe enables or disables rhythm mode and contains the
// percussion key-on bits.
if(address == 0xe) {
const bool old_rhythm_mode = rhythm_mode_enabled_;
rhythm_mode_enabled_ = value & 0x20;
if(old_rhythm_mode != rhythm_mode_enabled_) {
// Change the instlled instruments for channels 6, 7 and 8
// if this was a transition into or out of rhythm mode.
install_instrument(6);
install_instrument(7);
install_instrument(8);
}
rhythm_envelope_generators_[HighHat].set_key_on(value & 0x01);
rhythm_envelope_generators_[Cymbal].set_key_on(value & 0x02);
rhythm_envelope_generators_[TomTom].set_key_on(value & 0x04);
rhythm_envelope_generators_[Snare].set_key_on(value & 0x08);
if(value & 0x10) {
rhythm_envelope_generators_[BassCarrier].set_key_on(true);
} else {
rhythm_envelope_generators_[BassCarrier].set_key_on(false);
rhythm_envelope_generators_[BassModulator].set_key_on(false);
}
return;
}
// That leaves only per-channel selections, for which the addressing
// is completely orthogonal; check that a valid channel is being requested.
const auto index = address & 0xf;
if(index > 8) return;
switch(address & 0xf0) {
default: break;
// Address 1x sets the low 8 bits of the period for channel x.
case 0x10:
channels_[index].period = (channels_[index].period & ~0xff) | value;
set_channel_period(index);
return;
// Address 2x Sets the octave and a single bit of the frequency, as well
// as setting key on and sustain mode.
case 0x20:
channels_[index].period = (channels_[index].period & 0xff) | ((value & 1) << 8);
channels_[index].octave = (value >> 1) & 7;
set_channel_period(index);
// In this implementation the first 9 envelope generators are for
// channel carriers, and their will_attack callback is used to trigger
// key-on for modulators. But key-off needs to be set to both envelope
// generators now.
if(value & 0x10) {
envelope_generators_[index].set_key_on(true);
} else {
envelope_generators_[index + 0].set_key_on(false);
envelope_generators_[index + 9].set_key_on(false);
}
// Set sustain bit to both the relevant operators.
channels_[index].use_sustain = value & 0x20;
set_use_sustain(index);
return;
// Address 3x selects the instrument and attenuation for a channel;
// in rhythm mode some of the nibbles that ordinarily identify instruments
// instead nominate additional attenuations. This code reads those back
// from the stored instrument values.
case 0x30:
channels_[index].attenuation = value & 0xf;
// Install an instrument only if it's new.
if(channels_[index].instrument != value >> 4) {
channels_[index].instrument = value >> 4;
if(index < 6 || !rhythm_mode_enabled_) {
install_instrument(index);
}
}
return;
}
});
}
void OPLL::set_channel_period(int channel) {
phase_generators_[channel + 0].set_period(channels_[channel].period, channels_[channel].octave);
phase_generators_[channel + 9].set_period(channels_[channel].period, channels_[channel].octave);
envelope_generators_[channel + 0].set_period(channels_[channel].period, channels_[channel].octave);
envelope_generators_[channel + 9].set_period(channels_[channel].period, channels_[channel].octave);
key_level_scalers_[channel + 0].set_period(channels_[channel].period, channels_[channel].octave);
key_level_scalers_[channel + 9].set_period(channels_[channel].period, channels_[channel].octave);
}
const uint8_t *OPLL::instrument_definition(int instrument, int channel) {
// Divert to the appropriate rhythm instrument if in rhythm mode.
if(channel >= 6 && rhythm_mode_enabled_) {
return &percussion_patch_set[(channel - 6) * 8];
}
// Instrument 0 is the custom instrument.
if(!instrument) return custom_instrument_;
// Instruments other than 0 are taken from the fixed set.
const int index = (instrument - 1) * 8;
return is_vrc7_ ? &vrc7_patch_set[index] : &opll_patch_set[index];
}
void OPLL::install_instrument(int channel) {
auto &carrier_envelope = envelope_generators_[channel + 0];
auto &carrier_phase = phase_generators_[channel + 0];
auto &carrier_scaler = key_level_scalers_[channel + 0];
auto &modulator_envelope = envelope_generators_[channel + 9];
auto &modulator_phase = phase_generators_[channel + 9];
auto &modulator_scaler = key_level_scalers_[channel + 9];
const uint8_t *const instrument = instrument_definition(channels_[channel].instrument, channel);
// Bytes 0 (modulator) and 1 (carrier):
//
// b0-b3: multiplier;
// b4: key-scale rate enable;
// b5: sustain-level enable;
// b6: vibrato enable;
// b7: tremolo enable.
modulator_phase.set_multiple(instrument[0] & 0xf);
channels_[channel].modulator_key_rate_scale_multiplier = (instrument[0] >> 4) & 1;
modulator_phase.set_vibrato_enabled(instrument[0] & 0x40);
modulator_envelope.set_tremolo_enabled(instrument[0] & 0x80);
carrier_phase.set_multiple(instrument[1] & 0xf);
channels_[channel].carrier_key_rate_scale_multiplier = (instrument[1] >> 4) & 1;
carrier_phase.set_vibrato_enabled(instrument[1] & 0x40);
carrier_envelope.set_tremolo_enabled(instrument[1] & 0x80);
// Pass off bit 5.
set_use_sustain(channel);
// Byte 2:
//
// b0b5: modulator attenuation;
// b6b7: modulator key-scale level.
modulator_scaler.set_key_scaling_level(instrument[3] >> 6);
channels_[channel].modulator_attenuation = instrument[2] & 0x3f;
// Byte 3:
//
// b0b2: modulator feedback level;
// b3: modulator waveform selection;
// b4: carrier waveform selection;
// b5: [unused]
// b6b7: carrier key-scale level.
channels_[channel].modulator_feedback = instrument[3] & 7;
channels_[channel].modulator_waveform = Waveform((instrument[3] >> 3) & 1);
channels_[channel].carrier_waveform = Waveform((instrument[3] >> 4) & 1);
carrier_scaler.set_key_scaling_level(instrument[3] >> 6);
// Bytes 4 (modulator) and 5 (carrier):
//
// b0b3: decay rate;
// b4b7: attack rate.
modulator_envelope.set_decay_rate(instrument[4] & 0xf);
modulator_envelope.set_attack_rate(instrument[4] >> 4);
carrier_envelope.set_decay_rate(instrument[5] & 0xf);
carrier_envelope.set_attack_rate(instrument[5] >> 4);
// Bytes 6 (modulator) and 7 (carrier):
//
// b0b3: release rate;
// b4b7: sustain level.
modulator_envelope.set_release_rate(instrument[6] & 0xf);
modulator_envelope.set_sustain_level(instrument[6] >> 4);
carrier_envelope.set_release_rate(instrument[7] & 0xf);
carrier_envelope.set_sustain_level(instrument[7] >> 4);
}
void OPLL::set_use_sustain(int channel) {
const uint8_t *const instrument = instrument_definition(channels_[channel].instrument, channel);
envelope_generators_[channel + 0].set_use_sustain_level((instrument[1] & 0x20) || channels_[channel].use_sustain);
envelope_generators_[channel + 9].set_use_sustain_level((instrument[0] & 0x20) || channels_[channel].use_sustain);
}
// MARK: - Output generation.
void OPLL::set_sample_volume_range(std::int16_t range) {
total_volume_ = range;
}
void OPLL::get_samples(std::size_t number_of_samples, std::int16_t *target) {
// Both the OPLL and the OPL2 divide the input clock by 72 to get the base tick frequency;
// unlike the OPL2 the OPLL time-divides the output for 'mixing'.
const int update_period = 72 / audio_divider_;
const int channel_output_period = 4 / audio_divider_;
// TODO: the conditional below is terrible. Fix.
while(number_of_samples--) {
if(!audio_offset_) update_all_channels();
*target = output_levels_[audio_offset_ / channel_output_period];
++target;
audio_offset_ = (audio_offset_ + 1) % update_period;
}
}
void OPLL::update_all_channels() {
oscillator_.update();
// Update all phase generators. That's guaranteed.
for(int c = 0; c < 18; ++c) {
phase_generators_[c].update(oscillator_);
}
// Update the ADSR envelopes that are guaranteed to be melodic.
for(int c = 0; c < 6; ++c) {
envelope_generators_[c + 0].update(oscillator_);
envelope_generators_[c + 9].update(oscillator_);
}
#define VOLUME(x) int16_t(((x) * total_volume_) >> 12)
if(rhythm_mode_enabled_) {
// Advance the rhythm envelope generators.
for(int c = 0; c < 6; ++c) {
rhythm_envelope_generators_[c].update(oscillator_);
}
// Fill in the melodic channels.
output_levels_[3] = VOLUME(melodic_output(0));
output_levels_[4] = VOLUME(melodic_output(1));
output_levels_[5] = VOLUME(melodic_output(2));
output_levels_[9] = VOLUME(melodic_output(3));
output_levels_[10] = VOLUME(melodic_output(4));
output_levels_[11] = VOLUME(melodic_output(5));
// Bass drum, which is a regular FM effect.
output_levels_[2] = output_levels_[15] = VOLUME(bass_drum());
oscillator_.update_lfsr();
// Tom tom, which is a single operator.
output_levels_[1] = output_levels_[14] = VOLUME(tom_tom());
oscillator_.update_lfsr();
// Snare.
output_levels_[6] = output_levels_[16] = VOLUME(snare_drum());
oscillator_.update_lfsr();
// Cymbal.
output_levels_[7] = output_levels_[17] = VOLUME(cymbal());
oscillator_.update_lfsr();
// High-hat.
output_levels_[0] = output_levels_[13] = VOLUME(high_hat());
oscillator_.update_lfsr();
// Unutilised slots.
output_levels_[8] = output_levels_[12] = 0;
oscillator_.update_lfsr();
} else {
for(int c = 6; c < 9; ++c) {
envelope_generators_[c + 0].update(oscillator_);
envelope_generators_[c + 9].update(oscillator_);
}
// All melodic. Fairly easy.
output_levels_[0] = output_levels_[1] = output_levels_[2] =
output_levels_[6] = output_levels_[7] = output_levels_[8] =
output_levels_[12] = output_levels_[13] = output_levels_[14] = 0;
output_levels_[3] = VOLUME(melodic_output(0));
output_levels_[4] = VOLUME(melodic_output(1));
output_levels_[5] = VOLUME(melodic_output(2));
output_levels_[9] = VOLUME(melodic_output(3));
output_levels_[10] = VOLUME(melodic_output(4));
output_levels_[11] = VOLUME(melodic_output(5));
output_levels_[15] = VOLUME(melodic_output(6));
output_levels_[16] = VOLUME(melodic_output(7));
output_levels_[17] = VOLUME(melodic_output(8));
}
#undef VOLUME
// TODO: batch updates of the LFSR.
}
// TODO: verify attenuation scales pervasively below.
int OPLL::melodic_output(int channel) {
// The modulator always updates after the carrier, oddly enough. So calculate actual output first, based on the modulator's last value.
auto carrier = WaveformGenerator<period_precision>::wave(channels_[channel].carrier_waveform, phase_generators_[channel].scaled_phase(), channels_[channel].modulator_output);
carrier += envelope_generators_[channel].attenuation() + (channels_[channel].attenuation << 7) + key_level_scalers_[channel].attenuation();
// Get the modulator's new value.
auto modulation = WaveformGenerator<period_precision>::wave(channels_[channel].modulator_waveform, phase_generators_[channel + 9].phase());
modulation += envelope_generators_[channel + 9].attenuation() + (channels_[channel].modulator_attenuation << 5) + key_level_scalers_[channel + 9].attenuation();
// Apply feedback, if any.
phase_generators_[channel + 9].apply_feedback(channels_[channel].modulator_output, modulation, channels_[channel].modulator_feedback);
channels_[channel].modulator_output = modulation;
return carrier.level();
}
int OPLL::bass_drum() {
// Use modulator 6 and carrier 6, attenuated as per the bass-specific envelope generators and the attenuation level for channel 6.
auto modulation = WaveformGenerator<period_precision>::wave(Waveform::Sine, phase_generators_[6 + 9].phase());
modulation += rhythm_envelope_generators_[RhythmIndices::BassModulator].attenuation();
auto carrier = WaveformGenerator<period_precision>::wave(Waveform::Sine, phase_generators_[6].scaled_phase(), modulation);
carrier += rhythm_envelope_generators_[RhythmIndices::BassCarrier].attenuation() + (channels_[6].attenuation << 7);
return carrier.level();
}
int OPLL::tom_tom() {
// Use modulator 8 and the 'instrument' selection for channel 8 as an attenuation.
auto tom_tom = WaveformGenerator<period_precision>::wave(Waveform::Sine, phase_generators_[8 + 9].phase());
tom_tom += rhythm_envelope_generators_[RhythmIndices::TomTom].attenuation();
tom_tom += channels_[8].instrument << 7;
return tom_tom.level();
}
int OPLL::snare_drum() {
// Use modulator 7 and the carrier attenuation level for channel 7.
LogSign snare = WaveformGenerator<period_precision>::snare(oscillator_, phase_generators_[7 + 9].phase());
snare += rhythm_envelope_generators_[RhythmIndices::Snare].attenuation();
snare += channels_[7].attenuation << 7;
return snare.level();
}
int OPLL::cymbal() {
// Use modulator 7, carrier 8 and the attenuation level for channel 8.
LogSign cymbal = WaveformGenerator<period_precision>::cymbal(phase_generators_[8].phase(), phase_generators_[7 + 9].phase());
cymbal += rhythm_envelope_generators_[RhythmIndices::Cymbal].attenuation();
cymbal += channels_[8].attenuation << 7;
return cymbal.level();
}
int OPLL::high_hat() {
// Use modulator 7, carrier 8 a and the 'instrument' selection for channel 7 as an attenuation.
LogSign high_hat = WaveformGenerator<period_precision>::high_hat(oscillator_, phase_generators_[8].phase(), phase_generators_[7 + 9].phase());
high_hat += rhythm_envelope_generators_[RhythmIndices::HighHat].attenuation();
high_hat += channels_[7].instrument << 7;
return high_hat.level();
}

131
Components/OPx/OPLL.hpp Normal file
View File

@ -0,0 +1,131 @@
//
// OPLL.hpp
// Clock Signal
//
// Created by Thomas Harte on 03/05/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef OPLL_hpp
#define OPLL_hpp
#include "Implementation/OPLBase.hpp"
#include "Implementation/EnvelopeGenerator.hpp"
#include "Implementation/KeyLevelScaler.hpp"
#include "Implementation/PhaseGenerator.hpp"
#include "Implementation/LowFrequencyOscillator.hpp"
#include "Implementation/WaveformGenerator.hpp"
#include <atomic>
namespace Yamaha {
namespace OPL {
class OPLL: public OPLBase<OPLL> {
public:
/// Creates a new OPLL or VRC7.
OPLL(Concurrency::DeferringAsyncTaskQueue &task_queue, int audio_divider = 1, bool is_vrc7 = false);
/// As per ::SampleSource; provides audio output.
void get_samples(std::size_t number_of_samples, std::int16_t *target);
void set_sample_volume_range(std::int16_t range);
// The OPLL is generally 'half' as loud as it's told to be. This won't strictly be true in
// rhythm mode, but it's correct for melodic output.
double get_average_output_peak() const { return 0.5; }
/// Reads from the OPL.
uint8_t read(uint16_t address);
private:
friend OPLBase<OPLL>;
void write_register(uint8_t address, uint8_t value);
int audio_divider_ = 0;
int audio_offset_ = 0;
std::atomic<int> total_volume_;
int16_t output_levels_[18];
void update_all_channels();
int melodic_output(int channel);
int bass_drum();
int tom_tom();
int snare_drum();
int cymbal();
int high_hat();
static constexpr int period_precision = 9;
static constexpr int envelope_precision = 7;
// Standard melodic phase and envelope generators;
//
// These are assigned as:
//
// [x], 0 <= x < 9 = carrier for channel x;
// [x+9] = modulator for channel x.
//
PhaseGenerator<period_precision> phase_generators_[18];
EnvelopeGenerator<envelope_precision, period_precision> envelope_generators_[18];
KeyLevelScaler<period_precision> key_level_scalers_[18];
// Dedicated rhythm envelope generators and attenuations.
EnvelopeGenerator<envelope_precision, period_precision> rhythm_envelope_generators_[6];
enum RhythmIndices {
HighHat = 0,
Cymbal = 1,
TomTom = 2,
Snare = 3,
BassCarrier = 4,
BassModulator = 5
};
// Channel specifications.
struct Channel {
int octave = 0;
int period = 0;
int instrument = 0;
int attenuation = 0;
int modulator_attenuation = 0;
Waveform carrier_waveform = Waveform::Sine;
Waveform modulator_waveform = Waveform::Sine;
int carrier_key_rate_scale_multiplier = 0;
int modulator_key_rate_scale_multiplier = 0;
LogSign modulator_output;
int modulator_feedback = 0;
bool use_sustain = false;
} channels_[9];
// The low-frequency oscillator.
LowFrequencyOscillator oscillator_;
bool rhythm_mode_enabled_ = false;
bool is_vrc7_ = false;
// Contains the current configuration of the custom instrument.
uint8_t custom_instrument_[8] = {0, 0, 0, 0, 0, 0, 0, 0};
// Helpers to push per-channel information.
/// Pushes the current octave and period to channel @c channel.
void set_channel_period(int channel);
/// Installs the appropriate instrument on channel @c channel.
void install_instrument(int channel);
/// Sets whether the sustain level is used for channel @c channel based on its current instrument
/// and the user's selection.
void set_use_sustain(int channel);
/// @returns The 8-byte definition of instrument @c instrument.
const uint8_t *instrument_definition(int instrument, int channel);
};
}
}
#endif /* OPLL_hpp */

View File

@ -86,7 +86,7 @@ void SN76489::write(uint8_t value) {
});
}
bool SN76489::is_zero_level() {
bool SN76489::is_zero_level() const {
return channels_[0].volume == 0xf && channels_[1].volume == 0xf && channels_[2].volume == 0xf && channels_[3].volume == 0xf;
}

View File

@ -30,7 +30,7 @@ class SN76489: public Outputs::Speaker::SampleSource {
// As per SampleSource.
void get_samples(std::size_t number_of_samples, std::int16_t *target);
bool is_zero_level();
bool is_zero_level() const;
void set_sample_volume_range(std::int16_t range);
static constexpr bool get_is_stereo() { return false; }

View File

@ -55,7 +55,7 @@ void Audio::set_enabled(bool on) {
// MARK: - Output generation
bool Audio::is_zero_level() {
bool Audio::is_zero_level() const {
return !volume_ || !enabled_mask_;
}

View File

@ -53,7 +53,7 @@ class Audio: public ::Outputs::Speaker::SampleSource {
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter.
void get_samples(std::size_t number_of_samples, int16_t *target);
bool is_zero_level();
bool is_zero_level() const;
void set_sample_volume_range(std::int16_t range);
constexpr static bool get_is_stereo() { return false; }

View File

@ -12,6 +12,7 @@
#include "../../Components/9918/9918.hpp"
#include "../../Components/SN76489/SN76489.hpp"
#include "../../Components/OPx/OPLL.hpp"
#include "../MachineTypes.hpp"
#include "../../Configurable/Configurable.hpp"
@ -20,6 +21,7 @@
#include "../../ClockReceiver/JustInTime.hpp"
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "../../Outputs/Speaker/Implementation/CompoundSource.hpp"
#define LOG_PREFIX "[SMS] "
#include "../../Outputs/Log.hpp"
@ -30,7 +32,7 @@
#include <iostream>
namespace {
constexpr int sn76489_divider = 2;
constexpr int audio_divider = 1;
}
namespace Sega {
@ -96,12 +98,14 @@ class ConcreteMachine:
sn76489_(
(target.model == Target::Model::SG1000) ? TI::SN76489::Personality::SN76489 : TI::SN76489::Personality::SMS,
audio_queue_,
sn76489_divider),
speaker_(sn76489_),
audio_divider),
opll_(audio_queue_, audio_divider),
mixer_(sn76489_, opll_),
speaker_(mixer_),
keyboard_({Inputs::Keyboard::Key::Enter, Inputs::Keyboard::Key::Escape}, {}) {
// Pick the clock rate based on the region.
const double clock_rate = target.region == Target::Region::Europe ? 3546893.0 : 3579540.0;
speaker_.set_input_rate(static_cast<float>(clock_rate / sn76489_divider));
speaker_.set_input_rate(static_cast<float>(clock_rate / audio_divider));
set_clock_rate(clock_rate);
// Instantiate the joysticks.
@ -113,7 +117,9 @@ class ConcreteMachine:
map(write_pointers_, nullptr, 0x10000, 0);
// Take a copy of the cartridge and place it into memory.
cartridge_ = target.media.cartridges[0]->get_segments()[0].data;
if(!target.media.cartridges.empty()) {
cartridge_ = target.media.cartridges[0]->get_segments()[0].data;
}
if(cartridge_.size() < 48*1024) {
std::size_t new_space = 48*1024 - cartridge_.size();
cartridge_.resize(48*1024);
@ -134,7 +140,15 @@ class ConcreteMachine:
//
// 0072ed54 = US/European BIOS 1.3
// 48d44a13 = Japanese BIOS 2.1
const auto roms = rom_fetcher({ {"MasterSystem", "the Master System BIOS", "bios.sms", 8*1024, { 0x0072ed54, 0x48d44a13 } } });
const bool is_japanese = target.region == Target::Region::Japan;
const auto roms = rom_fetcher(
{ {"MasterSystem",
is_japanese ? "the Japanese Master System BIOS" : "the European/US Master System BIOS",
is_japanese ? "japanese-bios.sms" : "bios.sms",
8*1024,
{ is_japanese ? 0x48d44a13u : 0x0072ed54u }
} }
);
if(!roms[0]) {
// No BIOS found; attempt to boot as though it has already disabled itself.
memory_control_ |= 0x08;
@ -155,8 +169,12 @@ class ConcreteMachine:
map(write_pointers_, ram_, 1024, 0xc000, 0x10000);
}
// Apple a relatively low low-pass filter. More guidance needed here.
speaker_.set_high_frequency_cutoff(8000);
// Apply a relatively low low-pass filter. More guidance needed here.
// TODO: this is disabled for now since it isn't applicable for the FM chip, I think.
// speaker_.set_high_frequency_cutoff(8000);
// Set default mixer levels: FM off, SN full-throttle.
set_mixer_levels(0);
keyboard_.set_delegate(this);
}
@ -250,17 +268,29 @@ class ConcreteMachine:
time_until_interrupt_ = vdp_->get_time_until_interrupt();
break;
case 0xc0: {
Joystick *const joypad1 = static_cast<Joystick *>(joysticks_[0].get());
Joystick *const joypad2 = static_cast<Joystick *>(joysticks_[1].get());
*cycle.value = static_cast<uint8_t>(joypad1->get_state() | (joypad2->get_state() << 6));
if(memory_control_ & 0x4) {
if(has_fm_audio_ && (address & 0xff) == 0xf2) {
*cycle.value = opll_detection_word_;
} else {
*cycle.value = 0xff;
}
} else {
Joystick *const joypad1 = static_cast<Joystick *>(joysticks_[0].get());
Joystick *const joypad2 = static_cast<Joystick *>(joysticks_[1].get());
*cycle.value = static_cast<uint8_t>(joypad1->get_state() | (joypad2->get_state() << 6));
}
} break;
case 0xc1: {
Joystick *const joypad2 = static_cast<Joystick *>(joysticks_[1].get());
if(memory_control_ & 0x4) {
*cycle.value = 0xff;
} else {
Joystick *const joypad2 = static_cast<Joystick *>(joysticks_[1].get());
*cycle.value =
(joypad2->get_state() >> 2) |
0x30 |
get_th_values();
*cycle.value =
(joypad2->get_state() >> 2) |
0x30 |
get_th_values();
}
} break;
default:
@ -271,14 +301,15 @@ class ConcreteMachine:
case CPU::Z80::PartialMachineCycle::Output:
switch(address & 0xc1) {
case 0x00:
case 0x00: // i.e. even ports less than 0x40.
if(is_master_system(model_)) {
// TODO: Obey the RAM enable.
printf("Memory control: %02x\n", memory_control_);
memory_control_ = *cycle.value;
page_cartridge();
}
break;
case 0x01: {
case 0x01: { // i.e. odd ports less than 0x40.
// A programmer can force the TH lines to 0 here,
// causing a phoney lightgun latch, so check for any
// discontinuity in TH inputs.
@ -291,20 +322,28 @@ class ConcreteMachine:
vdp_->latch_horizontal_counter();
}
} break;
case 0x40: case 0x41:
case 0x40: case 0x41: // i.e. ports 0x400x7f.
update_audio();
sn76489_.write(*cycle.value);
break;
case 0x80: case 0x81:
case 0x80: case 0x81: // i.e. ports 0x800xbf.
vdp_->write(address, *cycle.value);
z80_.set_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
break;
case 0xc0:
LOG("TODO: [output] I/O port A/N; " << int(*cycle.value));
break;
case 0xc1:
LOG("TODO: [output] I/O port B/misc");
case 0xc1: case 0xc0: // i.e. ports 0xc00xff.
if(has_fm_audio_) {
switch(address & 0xff) {
case 0xf0: case 0xf1:
update_audio();
opll_.write(address, *cycle.value);
break;
case 0xf2:
opll_detection_word_ = *cycle.value;
set_mixer_levels(opll_detection_word_);
break;
}
}
break;
default:
@ -313,6 +352,19 @@ class ConcreteMachine:
}
break;
/*
TODO: implementation of the below is incomplete.
Re: io_port_control_
Set the TH pins for ports A and B as outputs. Set their output level
to any value desired by writing to bits 7 and 5. Read the state of both
TH pins back through bits 7 and 6 of port $DD. If the data returned is
the same as the data written, it's an export machine, otherwise it's
a domestic one.
Charles MacDonald
*/
case CPU::Z80::PartialMachineCycle::Interrupt:
*cycle.value = 0xff;
break;
@ -405,7 +457,32 @@ class ConcreteMachine:
}
inline void update_audio() {
speaker_.run_for(audio_queue_, time_since_sn76489_update_.divide_cycles(Cycles(sn76489_divider)));
speaker_.run_for(audio_queue_, time_since_sn76489_update_.divide_cycles(Cycles(audio_divider)));
}
void set_mixer_levels(uint8_t mode) {
// This is as per the audio control register;
// see https://www.smspower.org/Development/AudioControlPort
update_audio();
audio_queue_.defer([this, mode] {
switch(mode & 3) {
case 0: // SN76489 only; the default.
mixer_.set_relative_volumes({1.0f, 0.0f});
break;
case 1: // FM only.
mixer_.set_relative_volumes({0.0f, 1.0f});
break;
case 2: // No audio.
mixer_.set_relative_volumes({0.0f, 0.0f});
break;
case 3: // Both FM and SN76489.
mixer_.set_relative_volumes({0.5f, 0.5f});
break;
}
});
}
using Target = Analyser::Static::Sega::Target;
@ -417,7 +494,10 @@ class ConcreteMachine:
Concurrency::DeferringAsyncTaskQueue audio_queue_;
TI::SN76489 sn76489_;
Outputs::Speaker::LowpassSpeaker<TI::SN76489> speaker_;
Yamaha::OPL::OPLL opll_;
Outputs::Speaker::CompoundSource<decltype(sn76489_), decltype(opll_)> mixer_;
Outputs::Speaker::LowpassSpeaker<decltype(mixer_)> speaker_;
uint8_t opll_detection_word_ = 0xff;
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
Inputs::Keyboard keyboard_;
@ -433,6 +513,9 @@ class ConcreteMachine:
uint8_t io_port_control_ = 0x0f;
// This is a static constexpr for now; I may use it in the future.
static constexpr bool has_fm_audio_ = true;
// The memory map has a 1kb granularity; this is determined by the SG1000's 1kb of RAM.
const uint8_t *read_pointers_[64];
uint8_t *write_pointers_[64];

View File

@ -9,6 +9,9 @@
#ifndef LFSR_h
#define LFSR_h
#include <cstdint>
#include <cstdlib>
namespace Numeric {
template <typename IntType> struct LSFRPolynomial {};
@ -37,17 +40,28 @@ template <> struct LSFRPolynomial<uint8_t> {
*/
template <typename IntType = uint64_t, IntType polynomial = LSFRPolynomial<IntType>::value> class LFSR {
public:
/*!
Constructs an LFSR with a random initial value.
*/
LFSR() {
// Randomise the value, ensuring it doesn't end up being 0.
// Randomise the value, ensuring it doesn't end up being 0;
// don't set any top bits, in case this is a signed type.
while(!value_) {
uint8_t *value_byte = reinterpret_cast<uint8_t *>(&value_);
for(size_t c = 0; c < sizeof(IntType); ++c) {
*value_byte = uint8_t(uint64_t(rand()) * 255 / RAND_MAX);
*value_byte = uint8_t(uint64_t(rand()) * 127 / RAND_MAX);
++value_byte;
}
}
}
/*!
Constructs an LFSR with the specified initial value.
An initial value of 0 is invalid.
*/
LFSR(IntType initial_value) : value_(initial_value) {}
/*!
Advances the LSFR, returning either an @c IntType of value @c 1 or @c 0,
determining the bit that was just shifted out.

View File

@ -152,6 +152,7 @@
4B1D08061E0F7A1100763741 /* TimeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B1D08051E0F7A1100763741 /* TimeTests.mm */; };
4B1E85811D176468001EF87D /* 6532Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1E85801D176468001EF87D /* 6532Tests.swift */; };
4B1EDB451E39A0AC009D6819 /* chip.png in Resources */ = {isa = PBXBuildFile; fileRef = 4B1EDB431E39A0AC009D6819 /* chip.png */; };
4B2530F4244E6774007980BF /* fm.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B2530F3244E6773007980BF /* fm.json */; };
4B2A332D1DB86821002876E3 /* OricOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B2A332B1DB86821002876E3 /* OricOptions.xib */; };
4B2A539F1D117D36003C6002 /* CSAudioQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A53911D117D36003C6002 /* CSAudioQueue.m */; };
4B2B3A4B1F9B8FA70062DABF /* Typer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A471F9B8FA70062DABF /* Typer.cpp */; };
@ -777,12 +778,15 @@
4BBFBB6C1EE8401E00C01E7A /* ZX8081.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBFBB6A1EE8401E00C01E7A /* ZX8081.cpp */; };
4BBFE83D21015D9C00BF1C40 /* CSJoystickManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BBFE83C21015D9C00BF1C40 /* CSJoystickManager.m */; };
4BBFFEE61F7B27F1005F3FEB /* TrackSerialiser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBFFEE51F7B27F1005F3FEB /* TrackSerialiser.cpp */; };
4BC0CB282446BC7B00A79DBB /* OPLTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BC0CB272446BC7B00A79DBB /* OPLTests.mm */; };
4BC131702346DE5000E4FF3D /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC1316F2346DE5000E4FF3D /* StaticAnalyser.cpp */; };
4BC131712346DE5000E4FF3D /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC1316F2346DE5000E4FF3D /* StaticAnalyser.cpp */; };
4BC131762346DE9100E4FF3D /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC131752346DE9100E4FF3D /* StaticAnalyser.cpp */; };
4BC131772346DE9100E4FF3D /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC131752346DE9100E4FF3D /* StaticAnalyser.cpp */; };
4BC1317A2346DF2B00E4FF3D /* MSA.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC131782346DF2B00E4FF3D /* MSA.cpp */; };
4BC1317B2346DF2B00E4FF3D /* MSA.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC131782346DF2B00E4FF3D /* MSA.cpp */; };
4BC23A2C2467600F001A6030 /* OPLL.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC23A2B2467600E001A6030 /* OPLL.cpp */; };
4BC23A2D2467600F001A6030 /* OPLL.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC23A2B2467600E001A6030 /* OPLL.cpp */; };
4BC57CD92436A62900FBC404 /* State.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC57CD82436A62900FBC404 /* State.cpp */; };
4BC57CDA2436A62900FBC404 /* State.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC57CD82436A62900FBC404 /* State.cpp */; };
4BC5C3E022C994CD00795658 /* 68000MoveTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BC5C3DF22C994CC00795658 /* 68000MoveTests.mm */; };
@ -998,6 +1002,7 @@
4B1E85801D176468001EF87D /* 6532Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 6532Tests.swift; sourceTree = "<group>"; };
4B1EDB431E39A0AC009D6819 /* chip.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = chip.png; sourceTree = "<group>"; };
4B24095A1C45DF85004DA684 /* Stepper.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Stepper.hpp; sourceTree = "<group>"; };
4B2530F3244E6773007980BF /* fm.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = fm.json; sourceTree = "<group>"; };
4B2A332C1DB86821002876E3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Clock Signal/Base.lproj/OricOptions.xib"; sourceTree = SOURCE_ROOT; };
4B2A53901D117D36003C6002 /* CSAudioQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSAudioQueue.h; sourceTree = "<group>"; };
4B2A53911D117D36003C6002 /* CSAudioQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSAudioQueue.m; sourceTree = "<group>"; };
@ -1646,6 +1651,7 @@
4BBFE83C21015D9C00BF1C40 /* CSJoystickManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSJoystickManager.m; sourceTree = "<group>"; };
4BBFE83E21015DAE00BF1C40 /* CSJoystickManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CSJoystickManager.h; sourceTree = "<group>"; };
4BBFFEE51F7B27F1005F3FEB /* TrackSerialiser.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = TrackSerialiser.cpp; sourceTree = "<group>"; };
4BC0CB272446BC7B00A79DBB /* OPLTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = OPLTests.mm; sourceTree = "<group>"; };
4BC1316D2346DE5000E4FF3D /* StaticAnalyser.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = StaticAnalyser.hpp; sourceTree = "<group>"; };
4BC1316E2346DE5000E4FF3D /* Target.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Target.hpp; sourceTree = "<group>"; };
4BC1316F2346DE5000E4FF3D /* StaticAnalyser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StaticAnalyser.cpp; sourceTree = "<group>"; };
@ -1654,6 +1660,15 @@
4BC131752346DE9100E4FF3D /* StaticAnalyser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StaticAnalyser.cpp; sourceTree = "<group>"; };
4BC131782346DF2B00E4FF3D /* MSA.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = MSA.cpp; sourceTree = "<group>"; };
4BC131792346DF2B00E4FF3D /* MSA.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MSA.hpp; sourceTree = "<group>"; };
4BC23A222467600E001A6030 /* OPLL.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = OPLL.hpp; sourceTree = "<group>"; };
4BC23A242467600E001A6030 /* PhaseGenerator.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = PhaseGenerator.hpp; sourceTree = "<group>"; };
4BC23A252467600E001A6030 /* KeyLevelScaler.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = KeyLevelScaler.hpp; sourceTree = "<group>"; };
4BC23A262467600E001A6030 /* LowFrequencyOscillator.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = LowFrequencyOscillator.hpp; sourceTree = "<group>"; };
4BC23A272467600E001A6030 /* WaveformGenerator.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = WaveformGenerator.hpp; sourceTree = "<group>"; };
4BC23A282467600E001A6030 /* Tables.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Tables.hpp; sourceTree = "<group>"; };
4BC23A292467600E001A6030 /* OPLBase.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = OPLBase.hpp; sourceTree = "<group>"; };
4BC23A2A2467600E001A6030 /* EnvelopeGenerator.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = EnvelopeGenerator.hpp; sourceTree = "<group>"; };
4BC23A2B2467600E001A6030 /* OPLL.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = OPLL.cpp; sourceTree = "<group>"; };
4BC57CD2243427C700FBC404 /* AudioProducer.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = AudioProducer.hpp; sourceTree = "<group>"; };
4BC57CD32434282000FBC404 /* TimedMachine.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = TimedMachine.hpp; sourceTree = "<group>"; };
4BC57CD424342E0600FBC404 /* MachineTypes.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MachineTypes.hpp; sourceTree = "<group>"; };
@ -1951,9 +1966,7 @@
4B1414631B588A1100E04248 /* Test Binaries */ = {
isa = PBXGroup;
children = (
4B670A822401CB8400D4E002 /* Patrik Rak Z80 Tests */,
4B680CE323A555CA00451D43 /* 68000 Comparative Tests */,
4B85322B227793CA00F26553 /* TOS Startup */,
4B9252CD1E74D28200B76AF1 /* Atari ROMs */,
4B44EBF81DC9898E00A7820C /* BCDTEST_beeb */,
4B98A1CD1FFADEC400ADF63B /* MSX ROMs */,
@ -1961,8 +1974,11 @@
4B44EBF61DC9883B00A7820C /* 6502_functional_test.bin */,
4B44EBF41DC987AE00A7820C /* AllSuiteA.bin */,
4B9F11CB22729B3500701480 /* OPCLOGR2.BIN */,
4B2530F2244E6773007980BF /* FM Synthesis */,
4BBF49B41ED2881600AB3669 /* FUSE */,
4B670A822401CB8400D4E002 /* Patrik Rak Z80 Tests */,
4B9F11C72272375400701480 /* QL Startup */,
4B85322B227793CA00F26553 /* TOS Startup */,
4BB297E41B587D8300A49093 /* Wolfgang Lorenz 6502 test suite */,
4BE9A6B21EDE294200CBCB47 /* Zexall */,
);
@ -2034,6 +2050,14 @@
path = ../../SignalProcessing;
sourceTree = "<group>";
};
4B2530F2244E6773007980BF /* FM Synthesis */ = {
isa = PBXGroup;
children = (
4B2530F3244E6773007980BF /* fm.json */,
);
path = "FM Synthesis";
sourceTree = "<group>";
};
4B2A538F1D117D36003C6002 /* Audio */ = {
isa = PBXGroup;
children = (
@ -3335,6 +3359,7 @@
4BB73EB51B587A5100552FC2 /* Clock SignalTests */ = {
isa = PBXGroup;
children = (
4BC0CB272446BC7B00A79DBB /* OPLTests.mm */,
4B85322922778E4200F26553 /* Comparative68000.hpp */,
4B90467222C6FA31000E2074 /* TestRunner68000.hpp */,
4B97ADC722C6FD9B00A22A41 /* 68000ArithmeticTests.mm */,
@ -3509,6 +3534,30 @@
path = AtariST;
sourceTree = "<group>";
};
4BC23A212467600E001A6030 /* OPx */ = {
isa = PBXGroup;
children = (
4BC23A222467600E001A6030 /* OPLL.hpp */,
4BC23A232467600E001A6030 /* Implementation */,
4BC23A2B2467600E001A6030 /* OPLL.cpp */,
);
path = OPx;
sourceTree = "<group>";
};
4BC23A232467600E001A6030 /* Implementation */ = {
isa = PBXGroup;
children = (
4BC23A242467600E001A6030 /* PhaseGenerator.hpp */,
4BC23A252467600E001A6030 /* KeyLevelScaler.hpp */,
4BC23A262467600E001A6030 /* LowFrequencyOscillator.hpp */,
4BC23A272467600E001A6030 /* WaveformGenerator.hpp */,
4BC23A282467600E001A6030 /* Tables.hpp */,
4BC23A292467600E001A6030 /* OPLBase.hpp */,
4BC23A2A2467600E001A6030 /* EnvelopeGenerator.hpp */,
);
path = Implementation;
sourceTree = "<group>";
};
4BC57CD62436A61300FBC404 /* State */ = {
isa = PBXGroup;
children = (
@ -3537,6 +3586,7 @@
4B4A762D1DB1A35C007AAE2E /* AY38910 */,
4B302181208A550100773308 /* DiskII */,
4B4B1A39200198C900A0F866 /* KonamiSCC */,
4BC23A212467600E001A6030 /* OPx */,
4B0ACBFF237756EC008902D0 /* Serial */,
4BB0A6582044FD3000FB3688 /* SN76489 */,
);
@ -4252,6 +4302,7 @@
4BB299611B587D8400A49093 /* insax in Resources */,
4BB299351B587D8400A49093 /* cmpix in Resources */,
4B9F11C92272375400701480 /* qltrace.txt.gz in Resources */,
4B2530F4244E6774007980BF /* fm.json in Resources */,
4BB299041B587D8400A49093 /* aneb in Resources */,
4BB299BB1B587D8400A49093 /* rraa in Resources */,
4BB299091B587D8400A49093 /* aslz in Resources */,
@ -4416,6 +4467,7 @@
4B17B58C20A8A9D9007CCA8F /* StringSerialiser.cpp in Sources */,
4B055AA01FAE85DA0060FFFF /* MFMSectorDump.cpp in Sources */,
4BEBFB522002DB30000708CC /* DiskROM.cpp in Sources */,
4BC23A2D2467600F001A6030 /* OPLL.cpp in Sources */,
4B055AA11FAE85DA0060FFFF /* OricMFMDSK.cpp in Sources */,
4B0ACC2923775819008902D0 /* DMAController.cpp in Sources */,
4B055A951FAE85BB0060FFFF /* BitReverse.cpp in Sources */,
@ -4506,6 +4558,7 @@
4B4518A01F75FD1C00926311 /* CPCDSK.cpp in Sources */,
4BD424DF2193B5340097291A /* TextureTarget.cpp in Sources */,
4B0CCC451C62D0B3001CAC5F /* CRT.cpp in Sources */,
4BC23A2C2467600F001A6030 /* OPLL.cpp in Sources */,
4B322E041F5A2E3C004EB04C /* Z80Base.cpp in Sources */,
4B0ACC2623775819008902D0 /* AtariST.cpp in Sources */,
4B894530201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
@ -4713,6 +4766,7 @@
4B778F5523A5F2A70000D260 /* Keyboard.cpp in Sources */,
4B778F5D23A5F3230000D260 /* Commodore.cpp in Sources */,
4B98A05F1FFAD62400ADF63B /* CSROMFetcher.mm in Sources */,
4BC0CB282446BC7B00A79DBB /* OPLTests.mm in Sources */,
4BC9E1EE1D23449A003FCEE4 /* 6502InterruptTests.swift in Sources */,
4BEF6AAA1D35CE9E00E73575 /* DigitalPhaseLockedLoopBridge.mm in Sources */,
4B778F3123A5F0CB0000D260 /* Keyboard.cpp in Sources */,

View File

@ -23,9 +23,9 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Release"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
disableMainThreadChecker = "YES"
codeCoverageEnabled = "YES">
@ -67,7 +67,7 @@
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableASanStackUseAfterReturn = "YES"

View File

@ -23,7 +23,7 @@ static CPU::MOS6502::Register registerForRegister(CSTestMachine6502Register reg)
case CSTestMachine6502RegisterA: return CPU::MOS6502::Register::A;
case CSTestMachine6502RegisterX: return CPU::MOS6502::Register::X;
case CSTestMachine6502RegisterY: return CPU::MOS6502::Register::Y;
case CSTestMachine6502RegisterStackPointer: return CPU::MOS6502::Register::S;
case CSTestMachine6502RegisterStackPointer: return CPU::MOS6502::Register::StackPointer;
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,171 @@
//
// OPLTests.m
// Clock SignalTests
//
// Created by Thomas Harte on 14/04/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#import <XCTest/XCTest.h>
#include "Tables.hpp"
#include <cmath>
@interface OPLTests: XCTestCase
@end
@implementation OPLTests
// MARK: - Table tests
- (void)testSineLookup {
for(int c = 0; c < 1024; ++c) {
const auto logSin = Yamaha::OPL::negative_log_sin(c);
const auto level = Yamaha::OPL::power_two(logSin);
double fl_angle = double(c) * M_PI / 512.0;
double fl_level = sin(fl_angle);
XCTAssertLessThanOrEqual(fabs(fl_level - double(level) / 4096.0), 0.01, "Sine varies by more than 0.01 at angle %d", c);
}
}
// MARK: - Two-operator FM tests
/*- (void)compareFMTo:(NSArray *)knownGood atAttenuation:(int)attenuation {
Yamaha::OPL::Operator modulator, carrier;
Yamaha::OPL::Channel channel;
Yamaha::OPL::LowFrequencyOscillator oscillator;
// Set: AM = 0, PM = 0, EG = 1, KR = 0, MUL = 0
modulator.set_am_vibrato_hold_sustain_ksr_multiple(0x20);
carrier.set_am_vibrato_hold_sustain_ksr_multiple(0x20);
// Set: KL = 0, TL = 0
modulator.set_scaling_output(attenuation);
carrier.set_scaling_output(0);
// Set: waveform = 0.
modulator.set_waveform(0);
carrier.set_waveform(0);
// Set FB = 0, use FM synthesis.
channel.set_feedback_mode(1);
// Set: AR = 15, DR = 0.
modulator.set_attack_decay(0xf0);
carrier.set_attack_decay(0xf0);
// Set: SL = 0, RR = 15.
modulator.set_sustain_release(0x0f);
carrier.set_sustain_release(0x0f);
// Set 16384 samples for 1 period, and key-on.
channel.set_frequency_low(0x40);
channel.set_9bit_frequency_octave_key_on(0x10);
// Check one complete cycle of samples.
NSEnumerator *goodValues = [knownGood objectEnumerator];
for(int c = 0; c < 16384; ++c) {
channel.update(true, oscillator, modulator);
channel.update(false, oscillator, carrier);
const int generated = channel.melodic_output(modulator, carrier);
const int known = [[goodValues nextObject] intValue] >> 2;
XCTAssertLessThanOrEqual(abs(generated - known), 30, "FM synthesis varies by more than 10 at sample %d of attenuation %d", c, attenuation);
}
}
- (void)testFM {
// The following have been verified by sight against
// the images at https://www.smspower.org/Development/RE10
// as "close enough". Sadly the raw data isn't given, so
// that's the best as I can do. Fingers crossed!
NSURL *const url = [[NSBundle bundleForClass:[self class]] URLForResource:@"fm" withExtension:@"json"];
NSArray *const parent = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfURL:url] options:0 error:nil];
for(int c = 0; c < 64; ++c) {
[self compareFMTo:parent[c] atAttenuation:c];
}
}
// MARK: - Level tests
- (int)maxLevelForOPLLAttenuation:(int)attenuation {
Yamaha::OPL::Operator modulator, carrier;
Yamaha::OPL::Channel channel;
Yamaha::OPL::OperatorOverrides overrides;
Yamaha::OPL::LowFrequencyOscillator oscillator;
// Reach maximum volume immediately, and hold it during sustain.
carrier.set_sustain_release(0x0f);
carrier.set_attack_decay(0xf0);
// Use FM synthesis.
channel.set_feedback_mode(1);
// Set hold sustain level.
carrier.set_am_vibrato_hold_sustain_ksr_multiple(0x20);
// Disable the modulator.
modulator.set_scaling_output(0x3f);
// Set a non-zero frequency, set key on.
channel.set_frequency_low(0x40);
channel.set_9bit_frequency_octave_key_on(0x10);
// Get the maximum output for this volume level.
overrides.attenuation = attenuation;
overrides.use_sustain_level = true;
int max = 0;
for(int c = 0; c < 16384; ++c) {
channel.update(true, oscillator, modulator);
channel.update(false, oscillator, carrier, false, &overrides);
const int level = channel.melodic_output(modulator, carrier);
if(level > max) max = level;
}
return max;
}
- (void)testOPLLVolumeLevels {
// Get maximum output levels for all channels.
int maxLevels[16];
for(int c = 0; c < 16; ++c) {
maxLevels[c] = [self maxLevelForOPLLAttenuation:c];
}
// Figure out a divider to turn that into the sampled range.
const double multiplier = 255.0 / double(maxLevels[0]);
const double expectedLevels[16] = {255.0, 181.0, 127.0, 90.0, 63.0, 45.0, 31.0, 22.0, 15.0, 11.0, 7.0, 5.0, 3.0, 2.0, 1.0, 1.0};
for(int c = 0; c < 16; ++c) {
const double empiricalLevel = double(maxLevels[c]) * multiplier;
XCTAssertLessThanOrEqual(fabs(round(empiricalLevel) - expectedLevels[c]), 2.0, "Fixed attenuation %d was %0.0f; should have been %0.0f", c, empiricalLevel, expectedLevels[c]);
}
}
// MARK: - ADSR tests
- (void)testADSR {
// Yamaha::OPL::Operator test_operator;
// Yamaha::OPL::OperatorState test_state;
//
// test_operator.set_attack_decay(0x88);
// test_operator.set_sustain_release(0x88);
//
// // While key is off, output level should remain at 0.
// for(int c = 0; c < 1024; ++c) {
// test_operator.update(test_state, false, 0, 0, 0);
// XCTAssertGreaterThanOrEqual(test_state.level(), 0);
// }
//
// // Set key on...
// for(int c = 0; c < 4096; ++c) {
// test_operator.update(test_state, true, 0, 0, 0);
// NSLog(@"%d", test_state.level());
// }
}*/
@end

View File

@ -50,6 +50,7 @@ SOURCES += glob.glob('../../Components/AudioToggle/*.cpp')
SOURCES += glob.glob('../../Components/AY38910/*.cpp')
SOURCES += glob.glob('../../Components/DiskII/*.cpp')
SOURCES += glob.glob('../../Components/KonamiSCC/*.cpp')
SOURCES += glob.glob('../../Components/OPx/*.cpp')
SOURCES += glob.glob('../../Components/SN76489/*.cpp')
SOURCES += glob.glob('../../Components/Serial/*.cpp')

View File

@ -13,6 +13,7 @@
#include <cassert>
#include <cstring>
#include <atomic>
namespace Outputs {
namespace Speaker {
@ -26,7 +27,7 @@ template <typename... T> class CompoundSource:
public:
CompoundSource(T &... sources) : source_holder_(sources...) {
// Default: give all sources equal volume.
const float volume = 1.0f / static_cast<float>(source_holder_.size());
const auto volume = 1.0 / double(source_holder_.size());
for(std::size_t c = 0; c < source_holder_.size(); ++c) {
volumes_.push_back(volume);
}
@ -40,10 +41,12 @@ template <typename... T> class CompoundSource:
source_holder_.skip_samples(number_of_samples);
}
/*!
Sets the total output volume of this CompoundSource.
*/
void set_sample_volume_range(int16_t range) {
volume_range_ = range;
push_volumes();
source_holder_.set_scaled_volume_range(range, volumes_.data());
}
/*!
@ -51,17 +54,30 @@ template <typename... T> class CompoundSource:
compound. The caller should ensure that the number of items supplied
matches the number of sources and that the values in it sum to 1.0.
*/
void set_relative_volumes(const std::vector<float> &volumes) {
void set_relative_volumes(const std::vector<double> &volumes) {
assert(volumes.size() == source_holder_.size());
volumes_ = volumes;
push_volumes();
average_output_peak_ = 1.0 / source_holder_.total_scale(volumes_.data());
}
/*!
@returns true if any of the sources owned by this CompoundSource is stereo.
*/
static constexpr bool get_is_stereo() { return CompoundSourceHolder<T...>::get_is_stereo(); }
/*!
@returns the average output peak given the sources owned by this CompoundSource and the
current relative volumes.
*/
double get_average_output_peak() const {
return average_output_peak_;
}
private:
void push_volumes() {
source_holder_.set_scaled_volume_range(volume_range_, volumes_.data());
const double scale = source_holder_.total_scale(volumes_.data());
source_holder_.set_scaled_volume_range(volume_range_, volumes_.data(), scale);
}
template <typename... S> class CompoundSourceHolder: public Outputs::Speaker::SampleSource {
@ -70,15 +86,19 @@ template <typename... T> class CompoundSource:
std::memset(target, 0, sizeof(std::int16_t) * number_of_samples);
}
void set_scaled_volume_range(int16_t range, float *volumes) {}
void set_scaled_volume_range(int16_t range, double *volumes, double scale) {}
std::size_t size() {
static constexpr std::size_t size() {
return 0;
}
static constexpr bool get_is_stereo() {
return false;
}
double total_scale(double *) const {
return 0.0;
}
};
template <typename S, typename... R> class CompoundSourceHolder<S, R...> {
@ -125,27 +145,33 @@ template <typename... T> class CompoundSource:
next_source_.skip_samples(number_of_samples);
}
void set_scaled_volume_range(int16_t range, float *volumes) {
source_.set_sample_volume_range(static_cast<int16_t>(static_cast<float>(range * volumes[0])));
next_source_.set_scaled_volume_range(range, &volumes[1]);
void set_scaled_volume_range(int16_t range, double *volumes, double scale) {
const auto scaled_range = volumes[0] / double(source_.get_average_output_peak()) * double(range) / scale;
source_.set_sample_volume_range(int16_t(scaled_range));
next_source_.set_scaled_volume_range(range, &volumes[1], scale);
}
std::size_t size() {
return 1+next_source_.size();
static constexpr std::size_t size() {
return 1 + CompoundSourceHolder<R...>::size();
}
static constexpr bool get_is_stereo() {
return S::get_is_stereo() || CompoundSourceHolder<R...>::get_is_stereo();
}
double total_scale(double *volumes) const {
return (volumes[0] / source_.get_average_output_peak()) + next_source_.total_scale(&volumes[1]);
}
private:
S &source_;
CompoundSourceHolder<R...> next_source_;
};
CompoundSourceHolder<T...> source_holder_;
std::vector<float> volumes_;
std::vector<double> volumes_;
int16_t volume_range_ = 0;
std::atomic<double> average_output_peak_;
};
}

View File

@ -134,6 +134,8 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
const auto delegate = delegate_.load();
if(!delegate) return;
const int scale = get_scale();
std::size_t cycles_remaining = size_t(cycles.as_integral());
if(!cycles_remaining) return;
@ -156,6 +158,8 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
sample_source_.get_samples(cycles_to_read, &output_buffer_[output_buffer_pointer_ ]);
output_buffer_pointer_ += cycles_to_read * (SampleSource::get_is_stereo() ? 2 : 1);
// TODO: apply scale.
// Announce to delegate if full.
if(output_buffer_pointer_ == output_buffer_.size()) {
output_buffer_pointer_ = 0;
@ -174,7 +178,7 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
input_buffer_depth_ += cycles_to_read * (SampleSource::get_is_stereo() ? 2 : 1);
if(input_buffer_depth_ == input_buffer_.size()) {
resample_input_buffer();
resample_input_buffer(scale);
}
cycles_remaining -= cycles_to_read;
@ -246,6 +250,7 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
}
// Do something sensible with any dangling input, if necessary.
const int scale = get_scale();
switch(conversion_) {
// Neither direct copying nor resampling larger currently use any temporary input.
// Although in the latter case that's just because it's unimplemented. But, regardless,
@ -259,7 +264,7 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
const size_t required_buffer_size = size_t(number_of_taps) * (SampleSource::get_is_stereo() ? 2 : 1);
if(input_buffer_.size() != required_buffer_size) {
if(input_buffer_depth_ >= required_buffer_size) {
resample_input_buffer();
resample_input_buffer(scale);
input_buffer_depth_ %= required_buffer_size;
}
input_buffer_.resize(required_buffer_size);
@ -268,7 +273,7 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
}
}
inline void resample_input_buffer() {
inline void resample_input_buffer(int scale) {
if constexpr (SampleSource::get_is_stereo()) {
output_buffer_[output_buffer_pointer_ + 0] = filter_->apply(input_buffer_.data(), 2);
output_buffer_[output_buffer_pointer_ + 1] = filter_->apply(input_buffer_.data() + 1, 2);
@ -278,6 +283,18 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
output_buffer_pointer_++;
}
// Apply scale, if supplied, clamping appropriately.
if(scale != 65536) {
#define SCALE(x) x = int16_t(std::max(std::min((int(x) * scale) >> 16, 32767), -32768))
if constexpr (SampleSource::get_is_stereo()) {
SCALE(output_buffer_[output_buffer_pointer_ - 2]);
SCALE(output_buffer_[output_buffer_pointer_ - 1]);
} else {
SCALE(output_buffer_[output_buffer_pointer_ - 1]);
}
#undef SCALE
}
// Announce to delegate if full.
if(output_buffer_pointer_ == output_buffer_.size()) {
output_buffer_pointer_ = 0;
@ -301,6 +318,10 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
input_buffer_depth_ = 0;
}
}
int get_scale() {
return int(65536.0 / sample_source_.get_average_output_peak());
};
};
}

View File

@ -43,7 +43,7 @@ class SampleSource {
fill the target with zeroes; @c false if a call might return all zeroes or
might not.
*/
bool is_zero_level() {
bool is_zero_level() const {
return false;
}
@ -51,8 +51,20 @@ class SampleSource {
Sets the proper output range for this sample source; it should write values
between 0 and volume.
*/
void set_sample_volume_range(std::int16_t volume) {
}
void set_sample_volume_range(std::int16_t volume) {}
/*!
Indicates whether this component will write stereo samples.
*/
static constexpr bool get_is_stereo() { return false; }
/*!
Permits a sample source to declare that, averaged over time, it will use only
a certain proportion of the allocated volume range. This commonly happens
in sample sources that use a time-multiplexed sound output for example, if
one were to output only every other sample then it would return 0.5.
*/
double get_average_output_peak() const { return 1.0; }
};
}

View File

@ -2,4 +2,7 @@ BIOS files would ordinarily go here; the copyright status of these is uncertain
Expected files:
bios.sms — the EU/US Master System BIOS.
bios.sms — the EU/US Master System BIOS.
japanese-bios.sms — the Japanese Master System BIOS.
If the file sought by the emulator is missing, it will attempt to boot a game without using the BIOS.

View File

@ -122,7 +122,7 @@ template <typename Type> bool Reflection::get(const Struct &target, const std::s
}
template <typename Type> Type Reflection::get(const Struct &target, const std::string &name) {
Type value;
Type value{};
get(target, name, value);
return value;
}
@ -144,9 +144,7 @@ std::string Reflection::Struct::description() const {
// Output Bools as yes/no.
if(*type == typeid(bool)) {
bool value;
::Reflection::get(*this, key, value);
stream << (value ? "true" : "false");
stream << ::Reflection::get<bool>(*this, key);
continue;
}