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:
commit
5c1ae40a9c
@ -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;
|
||||
}
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
263
Components/OPx/Implementation/EnvelopeGenerator.hpp
Normal file
263
Components/OPx/Implementation/EnvelopeGenerator.hpp
Normal 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 0–15.
|
||||
*/
|
||||
void set_attack_rate(int rate) {
|
||||
attack_rate_ = rate << 2;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the decay rate, which should be in the range 0–15.
|
||||
*/
|
||||
void set_decay_rate(int rate) {
|
||||
decay_rate_ = rate << 2;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the release rate, which should be in the range 0–15.
|
||||
*/
|
||||
void set_release_rate(int rate) {
|
||||
release_rate_ = rate << 2;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sets the sustain level, which should be in the range 0–15.
|
||||
*/
|
||||
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 */
|
58
Components/OPx/Implementation/KeyLevelScaler.hpp
Normal file
58
Components/OPx/Implementation/KeyLevelScaler.hpp
Normal 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 */
|
68
Components/OPx/Implementation/LowFrequencyOscillator.hpp
Normal file
68
Components/OPx/Implementation/LowFrequencyOscillator.hpp
Normal 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 */
|
40
Components/OPx/Implementation/OPLBase.hpp
Normal file
40
Components/OPx/Implementation/OPLBase.hpp
Normal 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 */
|
125
Components/OPx/Implementation/PhaseGenerator.hpp
Normal file
125
Components/OPx/Implementation/PhaseGenerator.hpp
Normal 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 */
|
227
Components/OPx/Implementation/Tables.hpp
Normal file
227
Components/OPx/Implementation/Tables.hpp
Normal 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 */
|
92
Components/OPx/Implementation/WaveformGenerator.hpp
Normal file
92
Components/OPx/Implementation/WaveformGenerator.hpp
Normal 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
431
Components/OPx/OPLL.cpp
Normal 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:
|
||||
//
|
||||
// b0–b5: modulator attenuation;
|
||||
// b6–b7: modulator key-scale level.
|
||||
modulator_scaler.set_key_scaling_level(instrument[3] >> 6);
|
||||
channels_[channel].modulator_attenuation = instrument[2] & 0x3f;
|
||||
|
||||
// Byte 3:
|
||||
//
|
||||
// b0–b2: modulator feedback level;
|
||||
// b3: modulator waveform selection;
|
||||
// b4: carrier waveform selection;
|
||||
// b5: [unused]
|
||||
// b6–b7: 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):
|
||||
//
|
||||
// b0–b3: decay rate;
|
||||
// b4–b7: 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):
|
||||
//
|
||||
// b0–b3: release rate;
|
||||
// b4–b7: 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
131
Components/OPx/OPLL.hpp
Normal 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 */
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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_;
|
||||
}
|
||||
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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 0x40–0x7f.
|
||||
update_audio();
|
||||
sn76489_.write(*cycle.value);
|
||||
break;
|
||||
case 0x80: case 0x81:
|
||||
case 0x80: case 0x81: // i.e. ports 0x80–0xbf.
|
||||
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 0xc0–0xff.
|
||||
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];
|
||||
|
@ -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.
|
||||
|
@ -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 */,
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
66
OSBindings/Mac/Clock SignalTests/FM Synthesis/fm.json
Normal file
66
OSBindings/Mac/Clock SignalTests/FM Synthesis/fm.json
Normal file
File diff suppressed because one or more lines are too long
171
OSBindings/Mac/Clock SignalTests/OPLTests.mm
Normal file
171
OSBindings/Mac/Clock SignalTests/OPLTests.mm
Normal 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
|
@ -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')
|
||||
|
||||
|
@ -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_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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());
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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; }
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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.
|
@ -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;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user