diff --git a/ase/combo.cc b/ase/combo.cc index b018dd1c..e92c8beb 100644 --- a/ase/combo.cc +++ b/ase/combo.cc @@ -164,11 +164,31 @@ AudioChain::initialize (SpeakerArrangement busses) auto obus = add_output_bus ("Output", ospeakers_); (void) ibus; assert_return (OUT1 == obus); + + const double default_volume = 0.5407418735601; // -10dB + + ParameterMap pmap; + pmap.group = "Settings"; + pmap[VOLUME] = Param { "volume", _("Volume"), _("Volume"), default_volume, "", { 0, 1 }, GUIONLY }; + pmap[MUTE] = Param { "mute", _("Mute"), _("Mute"), false, "", {}, GUIONLY + ":toggle" }; + + ChoiceS solo_state_cs; + solo_state_cs += { "Off", "Solo is turned off" }; + solo_state_cs += { "On", "This track is solo" }; + solo_state_cs += { "Other", "Another track is solo" }; + pmap[SOLO_STATE] = Param { "solo_state", _("Solo State"), _("Solo State"), 0, "", std::move (solo_state_cs), GUIONLY }; + + install_params (pmap); + prepare_event_input(); } void AudioChain::reset (uint64 target_stamp) -{} +{ + volume_smooth_.reset (sample_rate(), 0.020); + reset_volume_ = true; + adjust_all_params(); +} uint AudioChain::schedule_children() @@ -189,6 +209,34 @@ AudioChain::schedule_children() void AudioChain::render (uint n_frames) { + bool volume_changed = false; + MidiEventInput evinput = midi_event_input(); + for (const auto &ev : evinput) + { + switch (ev.message()) + { + case MidiMessage::PARAM_VALUE: + apply_event (ev); + adjust_param (ev.param); + if (ev.param == VOLUME || ev.param == MUTE || ev.param == SOLO_STATE) + volume_changed = true; + break; + default: ; + } + } + if (volume_changed) + { + const int solo_state = irintf (get_param (SOLO_STATE)); + float new_volume = get_param (VOLUME); + if (solo_state == SOLO_STATE_OTHER) + new_volume = 0; + if (get_param (MUTE) && solo_state != SOLO_STATE_ON) + new_volume = 0; + // compute volume factor so that volume * volume * volume is in range [0..2] + const float cbrt_2 = 1.25992104989487; /* 2^(1/3) */ + volume_smooth_.set (new_volume * cbrt_2, reset_volume_); + reset_volume_ = false; + } // make the last processor output the chain output const size_t nlastchannels = last_output_ ? last_output_->n_ochannels (OUT1) : 0; const size_t n_och = n_ochannels (OUT1); @@ -205,12 +253,28 @@ AudioChain::render (uint n_frames) else { const float *cblock = last_output_->ofloats (OUT1, std::min (c, nlastchannels - 1)); - redirect_oblock (OUT1, c, cblock); + float *output_block = oblock (OUT1, c); + if (volume_smooth_.is_constant()) + { + float v = volume_smooth_.get_next(); + v = v * v * v; + for (uint i = 0; i < n_frames; i++) + output_block[i] = cblock[i] * v; + } + else + { + for (uint i = 0; i < n_frames; i++) + { + float v = volume_smooth_.get_next(); + v = v * v * v; + output_block[i] = cblock[i] * v; + } + } if (probes) { // SPL = 20 * log10 (root_mean_square (p) / p0) dB ; https://en.wikipedia.org/wiki/Sound_pressure#Sound_pressure_level // const float sqrsig = square_sum (n_frames, cblock) / n_frames; // * 1.0 / p0^2 - const float sqrsig = square_max (n_frames, cblock); + const float sqrsig = square_max (n_frames, output_block); const float log2div = 3.01029995663981; // 20 / log2 (10) / 2.0 const float db_spl = ISLIKELY (sqrsig > 0.0) ? log2div * fast_log2 (sqrsig) : -192; (*probes)[c].dbspl = db_spl; @@ -220,6 +284,26 @@ AudioChain::render (uint n_frames) // FIXME: assign obus if no children are present } +std::string +AudioChain::param_value_to_text (uint32_t paramid, double value) const +{ + if (paramid == VOLUME) + { + if (value > 0) + return string_format ("Volume %.1f dB", volume_db (value)); + else + return "Volume -\u221E dB"; + } + else + return AudioProcessor::param_value_to_text (paramid, value); +} + +float +AudioChain::volume_db (float volume) +{ + return voltage2db (2 * volume * volume * volume); +} + /// Reconnect AudioChain child processors at start and after. void AudioChain::reconnect (size_t index, bool insertion) diff --git a/ase/combo.hh b/ase/combo.hh index 3c4f9425..90d03e91 100644 --- a/ase/combo.hh +++ b/ase/combo.hh @@ -3,6 +3,7 @@ #define __ASE_COMBO_HH__ #include +#include namespace Ase { @@ -29,6 +30,9 @@ class AudioChain : public AudioCombo { const SpeakerArrangement ospeakers_ = SpeakerArrangement (0); InletP inlet_; AudioProcessor *last_output_ = nullptr; + static float volume_db (float volume); + LinearSmooth volume_smooth_; + bool reset_volume_ = false; protected: void initialize (SpeakerArrangement busses) override; void reset (uint64 target_stamp) override; @@ -43,6 +47,9 @@ public: using ProbeArray = std::array; ProbeArray* run_probes (bool enable); static void static_info (AudioProcessorInfo &info); + enum Params { VOLUME = 1, MUTE, SOLO_STATE }; + enum { SOLO_STATE_OFF, SOLO_STATE_ON, SOLO_STATE_OTHER }; + std::string param_value_to_text (uint32_t paramid, double value) const override; private: ProbeArray *probes_ = nullptr; bool probes_enabled_ = false; diff --git a/ase/properties.hh b/ase/properties.hh index c25791e4..36e43bde 100644 --- a/ase/properties.hh +++ b/ase/properties.hh @@ -61,7 +61,7 @@ class PropertyImpl : public ParameterProperty { public: ASE_DEFINE_MAKE_SHARED (PropertyImpl); Value get_value () override { Value v; getter_ (v); return v; } - bool set_value (const Value &v) override { return setter_ (v); } + bool set_value (const Value &v) override { bool changed = setter_ (v); if (changed) emit_notify (ident()); return changed; } ChoiceS choices () override { return lister_ ? lister_ (*this) : parameter_->choices(); } }; diff --git a/ase/track.cc b/ase/track.cc index 482737ac..e987fffa 100644 --- a/ase/track.cc +++ b/ase/track.cc @@ -77,6 +77,24 @@ TrackImpl::serialize (WritNode &xs) } // device chain xs["chain"] & *dynamic_cast (&*chain_); // always exists + /* TODO: while other properties on the track are not suitable for automation, + * the following properies are; so we will need a different serialization + * strategy for these to once we support automation + */ + for (auto prop : { "volume", "mute" }) + { + if (xs.in_save()) + { + Value v = get_value (prop); + xs[prop] & v; + } + if (xs.in_load()) + { + Value v; + xs[prop] & v; + set_value (prop, v); + } + } } void @@ -120,6 +138,7 @@ TrackImpl::_activate () DeviceImpl::_activate(); midi_prod_->_activate(); chain_->_activate(); + set_solo_states(); } void @@ -168,6 +187,74 @@ TrackImpl::midi_channel (int32 midichannel) // TODO: implement emit_notify ("midi_channel"); } +bool +TrackImpl::solo (bool new_solo) +{ + return_unless (new_solo != solo_, false); + solo_ = new_solo; + set_solo_states(); + emit_notify ("solo"); + return true; +} + +void +TrackImpl::set_solo_states() +{ + Ase::Project *project = dynamic_cast (_project()); + if (!project) + return; + + /* due to mute / solo, the volume of each track depends on its own volume and + * the mute/solo settings of all other tracks so we update all volumes + * together in this function (note: if we had automation we might want to do + * it differently if only one track volume changes) + */ + auto all_tracks = project->all_tracks(); + + bool have_solo_tracks = false; + for (const auto& track : all_tracks) + { + auto track_impl = dynamic_cast (track.get()); + have_solo_tracks = have_solo_tracks || track_impl->solo(); + } + + for (const auto& track : all_tracks) + { + auto track_impl = dynamic_cast (track.get()); + + Ase::AudioChain *audio_chain = dynamic_cast (&*track_impl->chain_->_audio_processor()); + if (track_impl->solo_) + audio_chain->send_param (Ase::AudioChain::SOLO_STATE, Ase::AudioChain::SOLO_STATE_ON); + else if (have_solo_tracks) + audio_chain->send_param (Ase::AudioChain::SOLO_STATE, Ase::AudioChain::SOLO_STATE_OTHER); + else + audio_chain->send_param (Ase::AudioChain::SOLO_STATE, Ase::AudioChain::SOLO_STATE_OFF); + } +} + +void +TrackImpl::create_properties () +{ + // chain to base class + DeviceImpl::create_properties(); + // create own properties + auto getsolo = [this] (Value &val) { val = solo(); }; + auto setsolo = [this] (const Value &val) { return solo (val.as_double()); }; + PropertyBag bag = property_bag(); + bag.group = _("Mix"); + bag += Prop (getsolo, setsolo, { "solo", _("Solo"), _("Solo"), false, "", {}, STANDARD + String (":toggle") }); +} + +PropertyS +TrackImpl::access_properties () +{ + PropertyS props = DeviceImpl::access_properties(); + PropertyS chain_props = chain_->access_properties(); + props.insert (props.end(), chain_props.begin(), chain_props.end()); + return props; +} + + static constexpr const uint MAX_LAUNCHER_CLIPS = 8; ClipS diff --git a/ase/track.hh b/ase/track.hh index 763bbf20..ca92bf47 100644 --- a/ase/track.hh +++ b/ase/track.hh @@ -11,12 +11,17 @@ class TrackImpl : public DeviceImpl, public virtual Track { DeviceP chain_, midi_prod_; ClipImplS clips_; uint midi_channel_ = 0; + bool solo_ = false; ASE_DEFINE_MAKE_SHARED (TrackImpl); friend class ProjectImpl; virtual ~TrackImpl (); + void set_solo_states (); protected: String fallback_name () const override; void serialize (WritNode &xs) override; + void create_properties () override; + bool solo () const { return solo_; } + bool solo (bool new_solo); public: class ClipScout; explicit TrackImpl (ProjectImpl&, bool masterflag); @@ -32,6 +37,7 @@ public: void midi_channel (int32 midichannel) override; ClipS launcher_clips () override; DeviceP access_device () override; + PropertyS access_properties () override; MonitorP create_monitor (int32 ochannel) override; void update_clips (); ssize_t clip_index (const ClipImpl &clip) const; diff --git a/ui/b/trackbutton.js b/ui/b/trackbutton.js new file mode 100644 index 00000000..17f27b53 --- /dev/null +++ b/ui/b/trackbutton.js @@ -0,0 +1,77 @@ +// This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 +// @ts-check + +import { LitComponent, html, JsExtract, live, docs, ref } from '../little.js'; +import * as Util from '../util.js'; + +/** @class BTrackButton + * @description + * The element is an editor for the track volume, implemented as thin wrapper + * arount . + * ### Properties: + * *label* + * : Either "M" for mute property or "S" for solo property + * *track* + * : The track + * *value* + * : Current value + */ + +//