From fa35e2d07b422d3b85559be20f54047d5f3e518e Mon Sep 17 00:00:00 2001 From: Stefan Westerfeld Date: Sat, 23 Sep 2023 18:51:55 +0200 Subject: [PATCH 01/15] ASE: implement mute/solo/volume for tracks Signed-off-by: Stefan Westerfeld --- ase/api.hh | 6 +++++ ase/combo.cc | 40 +++++++++++++++++++++++++++++--- ase/combo.hh | 5 ++++ ase/track.cc | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++ ase/track.hh | 10 ++++++++ 5 files changed, 123 insertions(+), 3 deletions(-) diff --git a/ase/api.hh b/ase/api.hh index a9cc4f38..723d5e4d 100644 --- a/ase/api.hh +++ b/ase/api.hh @@ -261,6 +261,12 @@ class Track : public virtual Device { public: virtual int32 midi_channel () const = 0; ///< Midi channel assigned to this track, 0 uses internal per-track channel. virtual void midi_channel (int32 midichannel) = 0; + virtual bool mute () const = 0; ///< Whether the track is muted + virtual void mute (bool newmute) = 0; + virtual bool solo () const = 0; ///< Whether the track is solo + virtual void solo (bool newsolo) = 0; + virtual double volume () const = 0; ///< Volume of the track [0..1] + virtual void volume (double newvolume) = 0; virtual bool is_master () const = 0; ///< Flag set on the main output track. virtual ClipS launcher_clips () = 0; ///< Retrieve the list of clips that can be directly played. virtual DeviceP access_device () = 0; ///< Retrieve Device handle for this track. diff --git a/ase/combo.cc b/ase/combo.cc index b018dd1c..9c1ed490 100644 --- a/ase/combo.cc +++ b/ase/combo.cc @@ -168,7 +168,10 @@ AudioChain::initialize (SpeakerArrangement busses) void AudioChain::reset (uint64 target_stamp) -{} +{ + volume_smooth_.reset (sample_rate(), 0.020); + volume_smooth_.set (volume_, true); +} uint AudioChain::schedule_children() @@ -205,12 +208,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 +239,21 @@ AudioChain::render (uint n_frames) // FIXME: assign obus if no children are present } +void +AudioChain::volume (float new_volume) +{ + /* compute volume factor so that volume_ * volume_ * volume_ is in range [0..2] */ + const float cbrt_2 = 1.25992104989487; /* 2^(1/3) */ + volume_ = new_volume * cbrt_2; + volume_smooth_.set (volume_); +} + +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..e8b3fea6 100644 --- a/ase/combo.hh +++ b/ase/combo.hh @@ -3,6 +3,7 @@ #define __ASE_COMBO_HH__ #include +#include namespace Ase { @@ -29,6 +30,8 @@ class AudioChain : public AudioCombo { const SpeakerArrangement ospeakers_ = SpeakerArrangement (0); InletP inlet_; AudioProcessor *last_output_ = nullptr; + float volume_ = 0; + LinearSmooth volume_smooth_; protected: void initialize (SpeakerArrangement busses) override; void reset (uint64 target_stamp) override; @@ -42,6 +45,8 @@ public: struct Probe { float dbspl = -192; }; using ProbeArray = std::array; ProbeArray* run_probes (bool enable); + void volume (float new_volume); + static float volume_db (float volume); static void static_info (AudioProcessorInfo &info); private: ProbeArray *probes_ = nullptr; diff --git a/ase/track.cc b/ase/track.cc index 482737ac..1054f9a5 100644 --- a/ase/track.cc +++ b/ase/track.cc @@ -120,6 +120,7 @@ TrackImpl::_activate () DeviceImpl::_activate(); midi_prod_->_activate(); chain_->_activate(); + set_chain_volumes(); } void @@ -168,6 +169,70 @@ TrackImpl::midi_channel (int32 midichannel) // TODO: implement emit_notify ("midi_channel"); } +void +TrackImpl::mute (bool new_mute) +{ + mute_ = new_mute; + set_chain_volumes(); + emit_notify ("mute"); +} + +void +TrackImpl::solo (bool new_solo) +{ + solo_ = new_solo; + set_chain_volumes(); + emit_notify ("solo"); +} + +void +TrackImpl::volume (double new_volume) +{ + volume_ = new_volume; + // TODO: display this value if track volume is changed in the UI + // printf ("Track '%s' -> set volume to %f dB\n", name().c_str(), AudioChain::volume_db (new_volume)); + set_chain_volumes(); + emit_notify ("volume"); +} + +void +TrackImpl::set_chain_volumes() +{ + 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) + have_solo_tracks = have_solo_tracks || track->solo(); + + for (const auto& track : all_tracks) + { + auto track_impl = dynamic_cast (track.get()); + + bool mute; + if (track_impl->solo_) // solo tracks are never muted + mute = false; + else if (have_solo_tracks) // if there are solo tracks all other tracks are muted + mute = true; + else // there isn't any solo track in the project, use mute from track + mute = track_impl->mute_; + + Ase::AudioChain *audio_chain = dynamic_cast (&*track_impl->chain_->_audio_processor()); + if (mute) + audio_chain->volume (0); + else + audio_chain->volume (track_impl->volume_); + } +} + static constexpr const uint MAX_LAUNCHER_CLIPS = 8; ClipS diff --git a/ase/track.hh b/ase/track.hh index 763bbf20..0a6863fd 100644 --- a/ase/track.hh +++ b/ase/track.hh @@ -11,9 +11,13 @@ class TrackImpl : public DeviceImpl, public virtual Track { DeviceP chain_, midi_prod_; ClipImplS clips_; uint midi_channel_ = 0; + bool mute_ = false; + bool solo_ = false; + double volume_ = 0.5407418735601; // -10dB ASE_DEFINE_MAKE_SHARED (TrackImpl); friend class ProjectImpl; virtual ~TrackImpl (); + void set_chain_volumes (); protected: String fallback_name () const override; void serialize (WritNode &xs) override; @@ -30,6 +34,12 @@ public: bool is_master () const override { return MASTER_TRACK & gadget_flags(); } int32 midi_channel () const override { return midi_channel_; } void midi_channel (int32 midichannel) override; + bool mute () const override { return mute_; } + void mute (bool new_mute) override; + bool solo () const override { return solo_; } + void solo (bool new_solo) override; + double volume () const override { return volume_; } + void volume (double new_volume) override; ClipS launcher_clips () override; DeviceP access_device () override; MonitorP create_monitor (int32 ochannel) override; From 225d8f1be56c29e195927316d6183a3196572044 Mon Sep 17 00:00:00 2001 From: Stefan Westerfeld Date: Fri, 6 Oct 2023 11:55:15 +0200 Subject: [PATCH 02/15] UI: b/*.js: add simple user interface for track mute/solo/volume Signed-off-by: Stefan Westerfeld --- ui/b/trackview.js | 9 ++ ui/b/trackvolume.js | 248 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 ui/b/trackvolume.js diff --git a/ui/b/trackview.js b/ui/b/trackview.js index 4bc38126..bbd70cca 100644 --- a/ui/b/trackview.js +++ b/ui/b/trackview.js @@ -61,6 +61,10 @@ b-trackview { .-track-name { display: inline-flex; position: relative; width: 7em; overflow: hidden; } + .-mute-solo { + display: flex; + flex-direction: row; + } } b-trackview[current-track] .b-trackview-control { background-color: zmod($b-button-border, Jz+=25%); @@ -76,6 +80,11 @@ const HTML = (t, d) => html` selectall @change=${event => t.track.name (event.detail.value.trim())} >${t.wtrack_.name} + + t.track.mute (event.target.value)} label="M"> + t.track.solo (event.target.value)} label="S"> + t.track.volume (event.target.value)}> +
t.levelbg_ = h)}>
t.covermid0_ = h)}>
diff --git a/ui/b/trackvolume.js b/ui/b/trackvolume.js new file mode 100644 index 00000000..b7371c43 --- /dev/null +++ b/ui/b/trackvolume.js @@ -0,0 +1,248 @@ +// 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 BTrackVolume + * @description + * The element is an editor for that track volume + * The input `value` will be constrained to take on an amount between `min` and `max` inclusively. + * ### Properties: + * *value* + * : Contains the number being edited. + * *track* + * : The track + * ### Events: + * *valuechange* + * : Event emitted whenever the volume changes. + */ + +//