// SPDX-License-Identifier: BSD-2-Clause // Copyright (C) 2020, The LabSound Authors. All rights reserved. #pragma once #ifndef AudioNode_h #define AudioNode_h #include "LabSound/core/Mixing.h" #include "LabSound/core/Profiler.h" #include #include #include #include namespace lab { // clang-format off enum PanningMode { PANNING_NONE = 0, EQUALPOWER = 1, HRTF = 2, _PanningModeCount }; enum FilterType { FILTER_NONE = 0, LOWPASS = 1, HIGHPASS = 2, BANDPASS = 3, LOWSHELF = 4, HIGHSHELF = 5, PEAKING = 6, NOTCH = 7, ALLPASS = 8, _FilterTypeCount }; enum OscillatorType { OSCILLATOR_NONE = 0, SINE = 1, FAST_SINE = 2, SQUARE = 3, SAWTOOTH = 4, TRIANGLE = 5, CUSTOM = 6, _OscillatorTypeCount }; enum class SchedulingState : int { UNSCHEDULED = 0, // Initial playback state. Created, but not yet scheduled SCHEDULED, // Scheduled to play (via noteOn() or noteGrainOn()), but not yet playing FADE_IN, // First epoch, fade in, then play PLAYING, // Generating sound STOPPING, // Transitioning to finished RESETTING, // Node is resetting to initial, unscheduled state FINISHING, // Playing has finished FINISHED // Node has finished }; const char* schedulingStateName(SchedulingState); // clang-format on class AudioContext; class AudioNodeInput; class AudioNodeOutput; class AudioParam; class AudioSetting; class ContextGraphLock; class ContextRenderLock; class AudioNodeScheduler { public: explicit AudioNodeScheduler() = delete; explicit AudioNodeScheduler(float sampleRate); ~AudioNodeScheduler() = default; // Scheduling. void start(double when); void stop(double when); void finish(ContextRenderLock&); // Called when there is no more sound to play or the noteOff/stop() time has been reached. void reset(); SchedulingState playbackState() const { return _playbackState; } bool hasFinished() const { return _playbackState == SchedulingState::FINISHED; } bool update(ContextRenderLock&, int epoch_length); SchedulingState _playbackState = SchedulingState::UNSCHEDULED; // epoch is a long count at sample rate; 136 years at 48kHz // For use in an interstellar sound installation or radio frequency signal processing, // please consider upgrading these to uint64_t or write some rollover logic. uint64_t _epoch = 0; // the epoch rendered currently in the busses uint64_t _epochLength = 0; // number of frames in current epoch uint64_t _startWhen = std::numeric_limits::max(); // requested start in epochal coordinate system uint64_t _stopWhen = std::numeric_limits::max(); // requested end in epochal coordinate system int _renderOffset = 0; // where rendering starts in the current frame int _renderLength = 0; // number of rendered frames in the current frame float _sampleRate = 1; std::function _onEnded; std::function _onStart; }; // An AudioNode is the basic building block for handling audio within an AudioContext. // It may be an audio source, an intermediate processing module, or an audio destination. // Each AudioNode can have inputs and/or outputs. // An AudioHardwareDeviceNode has one input and no outputs and represents the final destination to the audio hardware. // Most processing nodes such as filters will have one input and one output, although multiple inputs and outputs are possible. class AudioNode { public: enum : int { ProcessingSizeInFrames = 128 }; AudioNode() = delete; virtual ~AudioNode(); explicit AudioNode(AudioContext &); //-------------------------------------------------- // required interface // virtual const char* name() const = 0; // The AudioNodeInput(s) (if any) will already have their input data available when process() is called. // Subclasses will take this input data and put the results in the AudioBus(s) of its AudioNodeOutput(s) (if any). // Called from context's audio thread. virtual void process(ContextRenderLock &, int bufferSize) = 0; // Resets DSP processing state (clears delay lines, filter memory, etc.) // Called from context's audio thread. virtual void reset(ContextRenderLock &) = 0; // tailTime() is the length of time (not counting latency time) where non-zero output may occur after continuous silent input. virtual double tailTime(ContextRenderLock & r) const = 0; // latencyTime() is the length of time it takes for non-zero output to appear after non-zero input is provided. This only applies to // processing delay which is an artifact of the processing algorithm chosen and is *not* part of the intrinsic desired effect. For // example, a "delay" effect is expected to delay the signal, and thus would not be considered latency. virtual double latencyTime(ContextRenderLock & r) const = 0; //-------------------------------------------------- // required interface // // If the final node class has ScheduledNode in its class hierarchy, this will return true. // This is to save the cost of a dynamic_cast when scheduling nodes. virtual bool isScheduledNode() const { return false; } // No significant resources should be allocated until initialize() is called. // Processing may not occur until a node is initialized. virtual void initialize(); virtual void uninitialize(); bool isInitialized() const { return m_isInitialized; } // These locked versions can be called at run time. void addInput(ContextGraphLock&, std::unique_ptr input); void addOutput(ContextGraphLock&, std::unique_ptr output); int numberOfInputs() const { return static_cast(m_inputs.size()); } int numberOfOutputs() const { return static_cast(m_outputs.size()); } std::shared_ptr input(int index); std::shared_ptr output(int index); std::shared_ptr output(char const* const str); // processIfNecessary() is called by our output(s) when the rendering graph needs this AudioNode to process. // This method ensures that the AudioNode will only process once per rendering time quantum even if it's called repeatedly. // This handles the case of "fanout" where an output is connected to multiple AudioNode inputs. // Called from context's audio thread. void processIfNecessary(ContextRenderLock & r, int bufferSize); // Called when a new connection has been made to one of our inputs or the connection number of channels has changed. // This potentially gives us enough information to perform a lazy initialization or, if necessary, a re-initialization. // Called from main thread. virtual void checkNumberOfChannelsForInput(ContextRenderLock &, AudioNodeInput *); virtual void conformChannelCounts(); // propagatesSilence() should return true if the node will generate silent output when given silent input. By default, AudioNode // will take tailTime() and latencyTime() into account when determining whether the node will propagate silence. virtual bool propagatesSilence(ContextRenderLock & r) const; bool inputsAreSilent(ContextRenderLock &); void silenceOutputs(ContextRenderLock &); void unsilenceOutputs(ContextRenderLock &); int channelCount(); void setChannelCount(ContextGraphLock & g, int channelCount); ChannelCountMode channelCountMode() const { return m_channelCountMode; } void setChannelCountMode(ContextGraphLock & g, ChannelCountMode mode); ChannelInterpretation channelInterpretation() const { return m_channelInterpretation; } void setChannelInterpretation(ChannelInterpretation interpretation) { m_channelInterpretation = interpretation; } // returns a vector of parameter names std::vector paramNames() const; std::vector paramShortNames() const; // returns a vector of setting names std::vector settingNames() const; std::vector settingShortNames() const; std::shared_ptr param(char const * const str); std::shared_ptr setting(char const * const str); std::vector> params() const { return m_params; } std::vector> settings() const { return m_settings; } AudioNodeScheduler _scheduler; ProfileSample graphTime; // how much time the node spend pulling inputs ProfileSample totalTime; // total time spent by the node. total-graph is the self time. protected: // Inputs and outputs must be created before the AudioNode is initialized. // It is only legal to call this during a constructor. void addInput(std::unique_ptr input); void addOutput(std::unique_ptr output); // Called by processIfNecessary() to cause all parts of the rendering graph connected to us to process. // Each rendering quantum, the audio data for each of the AudioNode's inputs will be available after this method is called. // Called from context's audio thread. void pullInputs(ContextRenderLock &, int bufferSize); friend class AudioContext; bool m_isInitialized {false}; std::vector> m_inputs; std::vector> m_outputs; std::vector> m_params; std::vector> m_settings; int m_channelCount{ 0 }; ChannelCountMode m_channelCountMode{ ChannelCountMode::Max }; ChannelInterpretation m_channelInterpretation{ ChannelInterpretation::Speakers }; // starts an immediate ramp to zero in preparation for disconnection void scheduleDisconnect() { _scheduler.stop(0); } // returns true if the disconnection ramp has reached zero. // This is intended to allow the AudioContext to manage popping artifacts bool disconnectionReady() const { return _scheduler._playbackState != SchedulingState::PLAYING; } }; } // namespace lab #endif // AudioNode_h