diff --git a/Components/OPL2/Implementation/Channel.cpp b/Components/OPL2/Implementation/Channel.cpp index 1a780ff68..8dcb55f43 100644 --- a/Components/OPL2/Implementation/Channel.cpp +++ b/Components/OPL2/Implementation/Channel.cpp @@ -33,16 +33,16 @@ void Channel::set_feedback_mode(uint8_t value) { use_fm_synthesis_ = value & 1; } -int Channel::update(Operator *modulator, Operator *carrier, OperatorOverrides *modulator_overrides, OperatorOverrides *carrier_overrides) { +int Channel::update(const LowFrequencyOscillator &oscillator, Operator *modulator, Operator *carrier, OperatorOverrides *modulator_overrides, OperatorOverrides *carrier_overrides) { if(use_fm_synthesis_) { // Get modulator level, use that as a phase-adjusting input to the carrier and then return the carrier level. - modulator->update(modulator_state_, key_on_, period_ << frequency_shift_, octave_, nullptr, modulator_overrides); - carrier->update(carrier_state_, key_on_, period_ << frequency_shift_, octave_, &modulator_state_, carrier_overrides); + modulator->update(modulator_state_, nullptr, oscillator, key_on_, period_ << frequency_shift_, octave_, modulator_overrides); + carrier->update(carrier_state_, &modulator_state_, oscillator, key_on_, period_ << frequency_shift_, octave_, carrier_overrides); return carrier_state_.level(); } else { // Get modulator and carrier levels separately, return their sum. - modulator->update(modulator_state_, key_on_, period_ << frequency_shift_, octave_, nullptr, modulator_overrides); - carrier->update(carrier_state_, key_on_, period_ << frequency_shift_, octave_, nullptr, carrier_overrides); + modulator->update(modulator_state_, nullptr, oscillator, key_on_, period_ << frequency_shift_, octave_, modulator_overrides); + carrier->update(carrier_state_, nullptr, oscillator, key_on_, period_ << frequency_shift_, octave_, carrier_overrides); return (modulator_state_.level() + carrier_state_.level()); } } diff --git a/Components/OPL2/Implementation/Channel.hpp b/Components/OPL2/Implementation/Channel.hpp index a03483d34..122b051b6 100644 --- a/Components/OPL2/Implementation/Channel.hpp +++ b/Components/OPL2/Implementation/Channel.hpp @@ -9,6 +9,7 @@ #ifndef Channel_hpp #define Channel_hpp +#include "LowFrequencyOscillator.hpp" #include "Operator.hpp" namespace Yamaha { @@ -40,7 +41,7 @@ class Channel { /// This should be called at a rate of around 49,716 Hz; it returns the current output level /// level for this channel. - int update(Operator *modulator, Operator *carrier, OperatorOverrides *modulator_overrides = nullptr, OperatorOverrides *carrier_overrides = nullptr); + int update(const LowFrequencyOscillator &oscillator, Operator *modulator, Operator *carrier, OperatorOverrides *modulator_overrides = nullptr, OperatorOverrides *carrier_overrides = nullptr); /// @returns @c true if this channel is currently producing any audio; @c false otherwise; bool is_audible(Operator *carrier, OperatorOverrides *carrier_overrides = nullptr); diff --git a/Components/OPL2/Implementation/LowFrequencyOscillator.cpp b/Components/OPL2/Implementation/LowFrequencyOscillator.cpp new file mode 100644 index 000000000..0a7329213 --- /dev/null +++ b/Components/OPL2/Implementation/LowFrequencyOscillator.cpp @@ -0,0 +1,9 @@ +// +// LowFrequencyOscillator.cpp +// Clock Signal +// +// Created by Thomas Harte on 23/04/2020. +// Copyright © 2020 Thomas Harte. All rights reserved. +// + +#include "LowFrequencyOscillator.hpp" diff --git a/Components/OPL2/Implementation/LowFrequencyOscillator.hpp b/Components/OPL2/Implementation/LowFrequencyOscillator.hpp new file mode 100644 index 000000000..92c4ff5fc --- /dev/null +++ b/Components/OPL2/Implementation/LowFrequencyOscillator.hpp @@ -0,0 +1,54 @@ +// +// 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 + +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; + /// TODO + int vibrato = 0; + /// A counter of the number of operator update cycles (i.e. input clock / 72) since an arbitrary time. + int counter = 0; + + /// Updates the oscillator outputs + void update() { + ++counter; + + // Update tremolo. + ++tremolo_phase_; + + // 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 = (tremolo_phase_ >> 6) % 210; + const int tremolo_levels[2] = {tremolo_index >> 2, 52 - ((tremolo_index+1) >> 2)}; + tremolo = tremolo_levels[tremolo_index / 107]; + } + + private: + int tremolo_phase_ = 0; +}; + +} +} + +#endif /* LowFrequencyOscillator_hpp */ diff --git a/Components/OPL2/Implementation/Operator.cpp b/Components/OPL2/Implementation/Operator.cpp index 7bbfab936..242c723f3 100644 --- a/Components/OPL2/Implementation/Operator.cpp +++ b/Components/OPL2/Implementation/Operator.cpp @@ -62,7 +62,14 @@ bool Operator::is_audible(OperatorState &state, OperatorOverrides *overrides) { // MARK: - Update logic. -void Operator::update(OperatorState &state, bool key_on, int channel_period, int channel_octave, OperatorState *phase_offset, OperatorOverrides *overrides) { +void Operator::update( + OperatorState &state, + const OperatorState *phase_offset, + const LowFrequencyOscillator &oscillator, + bool key_on, + int channel_period, + int channel_octave, + const OperatorOverrides *overrides) { // Per the documentation: // // Delta phase = ( [desired freq] * 2^19 / [input clock / 72] ) / 2 ^ (b - 1) @@ -93,10 +100,9 @@ void Operator::update(OperatorState &state, bool key_on, int channel_period, int // On the leading edge of it becoming true, enter the attack state. if(!key_on) { state.adsr_phase_ = OperatorState::ADSRPhase::Release; - state.time_in_phase_ = 0; } else if(!state.last_key_on_) { state.adsr_phase_ = OperatorState::ADSRPhase::Attack; - state.time_in_phase_ = 0; + state.attack_time_ = 0; } state.last_key_on_ = key_on; @@ -110,8 +116,7 @@ void Operator::update(OperatorState &state, bool key_on, int channel_period, int assert(key_scaling_rate < 16); assert((channel_period >> 9) < 2); - const auto current_phase = state.adsr_phase_; - switch(current_phase) { + switch(state.adsr_phase_) { case OperatorState::ADSRPhase::Attack: { const int attack_rate = attack_rate_ + key_scaling_rate; @@ -125,7 +130,7 @@ void Operator::update(OperatorState &state, bool key_on, int channel_period, int state.adsr_attenuation_ = state.adsr_attenuation_ - (state.adsr_attenuation_ >> 2) - 1; } else { const int sample_length = 1 << (14 - (attack_rate >> 2)); // TODO: don't throw away KSR bits. - if(!(state.time_in_phase_ & (sample_length - 1))) { + if(!(state.attack_time_ & (sample_length - 1))) { state.adsr_attenuation_ = state.adsr_attenuation_ - (state.adsr_attenuation_ >> 3) - 1; } } @@ -162,7 +167,7 @@ void Operator::update(OperatorState &state, bool key_on, int channel_period, int case 2: state.adsr_attenuation_ += 16; break; default: { const int sample_length = 1 << ((decrease_rate >> 2) - 4); - if(!(state.time_in_phase_ & (sample_length - 1))) { + if(!(oscillator.counter & (sample_length - 1))) { state.adsr_attenuation_ += 8; } } break; @@ -183,11 +188,7 @@ void Operator::update(OperatorState &state, bool key_on, int channel_period, int // Nothing to do. break; } - if(state.adsr_phase_ == current_phase) { - ++state.time_in_phase_; - } else { - state.time_in_phase_ = 0; - } + ++state.attack_time_; // Calculate key-level scaling. Table is as per p14 of the YM3812 application manual, // converted into a fixed-point scheme. Compare with https://www.smspower.org/Development/RE12 @@ -221,4 +222,9 @@ void Operator::update(OperatorState &state, bool key_on, int channel_period, int // attenuations of 24db to 0.75db. state.attenuation.log += (state.adsr_attenuation_ << 3) + (attenuation_ << 5); } + + // Add optional tremolo. + state.attenuation.log += int(apply_amplitude_modulation_) * oscillator.tremolo << 4; } + +// TODO: both the tremolo and ADSR envelopes should be half-resolution on an OPLL. diff --git a/Components/OPL2/Implementation/Operator.hpp b/Components/OPL2/Implementation/Operator.hpp index 1cb58b643..97eb3c5c8 100644 --- a/Components/OPL2/Implementation/Operator.hpp +++ b/Components/OPL2/Implementation/Operator.hpp @@ -11,6 +11,7 @@ #include #include "Tables.hpp" +#include "LowFrequencyOscillator.hpp" namespace Yamaha { namespace OPL { @@ -30,7 +31,7 @@ struct OperatorState { enum class ADSRPhase { Attack, Decay, Sustain, Release } adsr_phase_ = ADSRPhase::Attack; - int time_in_phase_ = 0; + int attack_time_ = 0; int adsr_attenuation_ = 511; bool last_key_on_ = false; @@ -83,7 +84,14 @@ class Operator { void set_am_vibrato_hold_sustain_ksr_multiple(uint8_t value); /// Provides one clock tick to the operator, along with the relevant parameters of its channel. - void update(OperatorState &state, bool key_on, int channel_period, int channel_octave, OperatorState *phase_offset, OperatorOverrides *overrides = nullptr); + void update( + OperatorState &state, + const OperatorState *phase_offset, + const LowFrequencyOscillator &oscillator, + bool key_on, + int channel_period, + int channel_octave, + const OperatorOverrides *overrides = nullptr); /// @returns @c true if this channel currently has a non-zero output; @c false otherwise. bool is_audible(OperatorState &state, OperatorOverrides *overrides = nullptr); diff --git a/Components/OPL2/OPL2.cpp b/Components/OPL2/OPL2.cpp index a5a92d2ce..6587222a4 100644 --- a/Components/OPL2/OPL2.cpp +++ b/Components/OPL2/OPL2.cpp @@ -180,9 +180,12 @@ void OPLL::setup_fixed_instrument(int number, const uint8_t *data) { } void OPLL::update_all_chanels() { + // Update the LFO. + oscillator_.update(); + // Channels that are updated for melodic output regardless. for(int c = 0; c < 6; ++ c) { - channels_[c].level = (channels_[c].update() * total_volume_) >> 11; + channels_[c].level = (channels_[c].update(oscillator_) * total_volume_) >> 11; } if(depth_rhythm_control_ & 0x20) { @@ -190,7 +193,7 @@ void OPLL::update_all_chanels() { } else { // All melody, all the time. for(int c = 6; c < 9; ++ c) { - channels_[c].level = (channels_[c].update() * total_volume_) >> 11; + channels_[c].level = (channels_[c].update(oscillator_) * total_volume_) >> 11; } } diff --git a/Components/OPL2/OPL2.hpp b/Components/OPL2/OPL2.hpp index b77d698f5..8bae530a9 100644 --- a/Components/OPL2/OPL2.hpp +++ b/Components/OPL2/OPL2.hpp @@ -29,6 +29,7 @@ template class OPLBase: public ::Outputs::Speaker::SampleSource OPLBase(Concurrency::DeferringAsyncTaskQueue &task_queue); Concurrency::DeferringAsyncTaskQueue &task_queue_; + LowFrequencyOscillator oscillator_; uint8_t depth_rhythm_control_; uint8_t csm_keyboard_split_; @@ -92,9 +93,10 @@ struct OPLL: public OPLBase { // one user-configurable channel, 15 hard-coded channels, and // three channels configured for rhythm generation. + struct Channel: public ::Yamaha::OPL::Channel { - int update() { - return Yamaha::OPL::Channel::update(modulator, modulator + 1, nullptr, &overrides); + int update(const LowFrequencyOscillator &oscillator) { + return Yamaha::OPL::Channel::update(oscillator, modulator, modulator + 1, nullptr, &overrides); } bool is_audible() { diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 646d3daa5..62a4a224e 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -222,6 +222,8 @@ 4B595FAE2086DFBA0083CAA8 /* AudioToggle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B595FAC2086DFBA0083CAA8 /* AudioToggle.cpp */; }; 4B5FADBA1DE3151600AEC565 /* FileHolder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5FADB81DE3151600AEC565 /* FileHolder.cpp */; }; 4B5FADC01DE3BF2B00AEC565 /* Microdisc.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5FADBE1DE3BF2B00AEC565 /* Microdisc.cpp */; }; + 4B61909024526E640013F202 /* LowFrequencyOscillator.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B61908E24526E640013F202 /* LowFrequencyOscillator.cpp */; }; + 4B61909124526E640013F202 /* LowFrequencyOscillator.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B61908E24526E640013F202 /* LowFrequencyOscillator.cpp */; }; 4B622AE5222E0AD5008B59F2 /* DisplayMetrics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B622AE3222E0AD5008B59F2 /* DisplayMetrics.cpp */; }; 4B643F3A1D77AD1900D431D6 /* CSStaticAnalyser.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B643F391D77AD1900D431D6 /* CSStaticAnalyser.mm */; }; 4B643F3F1D77B88000D431D6 /* DocumentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B643F3E1D77B88000D431D6 /* DocumentController.swift */; }; @@ -1147,6 +1149,8 @@ 4B5FADB91DE3151600AEC565 /* FileHolder.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = FileHolder.hpp; sourceTree = ""; }; 4B5FADBE1DE3BF2B00AEC565 /* Microdisc.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Microdisc.cpp; path = Oric/Microdisc.cpp; sourceTree = ""; }; 4B5FADBF1DE3BF2B00AEC565 /* Microdisc.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Microdisc.hpp; path = Oric/Microdisc.hpp; sourceTree = ""; }; + 4B61908E24526E640013F202 /* LowFrequencyOscillator.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = LowFrequencyOscillator.cpp; sourceTree = ""; }; + 4B61908F24526E640013F202 /* LowFrequencyOscillator.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = LowFrequencyOscillator.hpp; sourceTree = ""; }; 4B622AE3222E0AD5008B59F2 /* DisplayMetrics.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = DisplayMetrics.cpp; path = ../../Outputs/DisplayMetrics.cpp; sourceTree = ""; }; 4B622AE4222E0AD5008B59F2 /* DisplayMetrics.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = DisplayMetrics.hpp; path = ../../Outputs/DisplayMetrics.hpp; sourceTree = ""; }; 4B643F381D77AD1900D431D6 /* CSStaticAnalyser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CSStaticAnalyser.h; path = StaticAnalyser/CSStaticAnalyser.h; sourceTree = ""; }; @@ -3526,6 +3530,8 @@ 4BC0CB352447EC9A00A79DBB /* Channel.cpp */, 4BC0CB362447EC9A00A79DBB /* Channel.hpp */, 4BC0CB3A2447ECAE00A79DBB /* Tables.hpp */, + 4B61908E24526E640013F202 /* LowFrequencyOscillator.cpp */, + 4B61908F24526E640013F202 /* LowFrequencyOscillator.hpp */, ); path = Implementation; sourceTree = ""; @@ -4423,6 +4429,7 @@ 4B055AB01FAE86070060FFFF /* PulseQueuedTape.cpp in Sources */, 4B055AAC1FAE85FD0060FFFF /* PCMSegment.cpp in Sources */, 4BB307BC235001C300457D33 /* 6850.cpp in Sources */, + 4B61909124526E640013F202 /* LowFrequencyOscillator.cpp in Sources */, 4B055AB31FAE860F0060FFFF /* CSW.cpp in Sources */, 4B89451D201967B4007DE474 /* Disk.cpp in Sources */, 4BDACBED22FFA5D20045EF7E /* ncr5380.cpp in Sources */, @@ -4743,6 +4750,7 @@ 4B1B88C0202E3DB200B67DFF /* MultiConfigurable.cpp in Sources */, 4BFF1D3922337B0300838EA1 /* 68000Storage.cpp in Sources */, 4B54C0BC1F8D8E790050900F /* KeyboardMachine.cpp in Sources */, + 4B61909024526E640013F202 /* LowFrequencyOscillator.cpp in Sources */, 4BB244D522AABAF600BE20E5 /* z8530.cpp in Sources */, 4BB73EA21B587A5100552FC2 /* AppDelegate.swift in Sources */, 4B894534201967B4007DE474 /* AddressMapper.cpp in Sources */,