mirror of
https://github.com/storytold/soundcraft.git
synced 2026-11-02 06:56:53 -06:00
* chore(lints): apply strict workspace Clippy rules * refactor(h264): split decoding work by operation * refactor(h264): clarify prediction and filter inputs * docs: require workspace lint inheritance * refactor(audio): clarify host FFI imports and inputs * docs: clarify crate lint placement * refactor(audio): replace wildcard imports in I/O modules * refactor(mix): extract strip and routing stages * refactor(ui): split edit window event handlers * refactor(ui): clarify app and dialog actions * refactor(ui): simplify mixer and panel workflows * fix(ui): apply track resize only once
536 lines
21 KiB
Rust
536 lines
21 KiB
Rust
use super::{
|
|
Arc, AtomicBool, BLOCK, BTreeMap, Cmd, HashMap, HashSet, MAX_CHANNELS, MeterSnapshot, MixEngine, OLD_SESSIONS, Ordering, OutputTrouble, Plugin,
|
|
PluginEditor, PluginStateReader, PluginStates, PreparedInstance, Range, Receiver, Samples, Sender, Session, Shared, SyncSender, TrackId,
|
|
itu_stereo_fold, main_channels, record, sync_channel,
|
|
};
|
|
|
|
/// How the audio output is doing, for the UI.
|
|
#[derive(Default)]
|
|
pub(crate) struct Fault {
|
|
/// A block panicked: output stays silent from then on.
|
|
failed: AtomicBool,
|
|
/// The device went away or the stream was invalidated: only a reconnect can restore audio.
|
|
pub(crate) gone: AtomicBool,
|
|
/// Blocks so far, to tell a device that stopped asking for audio.
|
|
pub(crate) beats: std::sync::atomic::AtomicU64,
|
|
/// The UI has been told.
|
|
reported: AtomicBool,
|
|
}
|
|
|
|
impl Fault {
|
|
/// What stopped the output, once.
|
|
pub(crate) fn trouble(&self, stalled: bool) -> Option<OutputTrouble> {
|
|
let trouble = if self.failed.load(Ordering::Relaxed) {
|
|
OutputTrouble::Bug
|
|
} else if self.gone.load(Ordering::Relaxed) {
|
|
OutputTrouble::Gone
|
|
} else if stalled {
|
|
OutputTrouble::Stalled
|
|
} else {
|
|
return None;
|
|
};
|
|
(!self.reported.swap(true, Ordering::Relaxed)).then_some(trouble)
|
|
}
|
|
}
|
|
|
|
/// How long a playing output may go without a block before it counts as stalled.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub(crate) const STALL: std::time::Duration = std::time::Duration::from_secs(2);
|
|
|
|
/// True once the block count `beats` has stood still for `STALL` while playing; `seen` is the
|
|
/// count last seen and since when.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub(crate) fn stalled(seen: &mut Option<(u64, std::time::Instant)>, beats: u64, playing: bool) -> bool {
|
|
match *seen {
|
|
Some((b, since)) if playing && b == beats => since.elapsed() >= STALL,
|
|
_ => {
|
|
*seen = Some((beats, std::time::Instant::now()));
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Fill one block with `fill`. If it panics, the block and every later one are silent and the
|
|
/// transport stops, rather than the panic taking the app down.
|
|
pub(crate) fn guarded_block(fault: &Fault, shared: &Shared, out: &mut [f32], fill: impl FnOnce(&mut [f32])) {
|
|
fault.beats.fetch_add(1, Ordering::Relaxed);
|
|
if fault.failed.load(Ordering::Relaxed) || std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| fill(out))).is_err() {
|
|
fault.failed.store(true, Ordering::Relaxed);
|
|
shared.playing.store(false, Ordering::Relaxed);
|
|
out.fill(0.0);
|
|
}
|
|
}
|
|
|
|
/// State owned by the audio callback.
|
|
pub(crate) struct AudioState {
|
|
rx: Receiver<Cmd>,
|
|
pub(crate) shared: Arc<Shared>,
|
|
pub(crate) session: Arc<Session>,
|
|
pub(crate) mix: MixEngine,
|
|
pos: Samples,
|
|
playing: bool,
|
|
end: Option<Samples>,
|
|
looped: Option<Range>,
|
|
/// Rendered frames waiting to be consumed (planar, main channels) and the fractional read index.
|
|
pending: Vec<Vec<f32>>,
|
|
/// Stereo fold-down gains per main channel (used when the device has fewer channels).
|
|
fold: [(f32, f32); MAX_CHANNELS],
|
|
pub(crate) pending_len: usize,
|
|
read: f64,
|
|
pub(crate) device_rate: f64,
|
|
input: Option<Arc<record::InputRing>>,
|
|
recording: bool,
|
|
/// Retired plugin instances go back to the UI thread to be dropped there.
|
|
garbage: Sender<Vec<Box<dyn Plugin>>>,
|
|
old_sessions: Option<SyncSender<Arc<Session>>>,
|
|
/// Meters of blocks rendered while the UI held the shared snapshot (`try_lock` failed), so
|
|
/// their peaks reach it with the next block instead of being lost.
|
|
pub(crate) unsent: MeterSnapshot,
|
|
/// Deleted tracks still to drop from the shared snapshot (it was locked when they went).
|
|
forget_shared: Vec<TrackId>,
|
|
}
|
|
|
|
fn new_engine(sr: f32) -> MixEngine {
|
|
let mut m = MixEngine::new(sr, BLOCK);
|
|
m.set_external_instances(true);
|
|
m.metronome = true;
|
|
m
|
|
}
|
|
|
|
impl AudioState {
|
|
pub(crate) fn new(
|
|
rx: Receiver<Cmd>,
|
|
shared: Arc<Shared>,
|
|
session: Arc<Session>,
|
|
device_rate: f64,
|
|
garbage: Sender<Vec<Box<dyn Plugin>>>,
|
|
) -> Self {
|
|
let sr = session.sample_rate.as_f64() as f32;
|
|
let nch = main_channels(&session);
|
|
AudioState {
|
|
garbage,
|
|
old_sessions: None,
|
|
fold: fold_gains(&session),
|
|
rx,
|
|
shared,
|
|
mix: new_engine(sr),
|
|
session,
|
|
pos: 0,
|
|
playing: false,
|
|
end: None,
|
|
looped: None,
|
|
pending: vec![vec![0.0; BLOCK]; nch],
|
|
pending_len: 0,
|
|
read: 0.0,
|
|
device_rate,
|
|
input: None,
|
|
recording: false,
|
|
unsent: MeterSnapshot::default(),
|
|
forget_shared: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn drain(&mut self) {
|
|
let mut changed = false;
|
|
while let Ok(c) = self.rx.try_recv() {
|
|
match c {
|
|
Cmd::Input(r) => self.input = Some(r),
|
|
Cmd::Forget(gone) => {
|
|
self.unsent.forget(&gone);
|
|
if self.forget_shared.is_empty() {
|
|
self.forget_shared = gone;
|
|
} else {
|
|
self.forget_shared.extend_from_slice(&gone);
|
|
}
|
|
self.forget_meters();
|
|
}
|
|
Cmd::Recording(r) => self.recording = r,
|
|
Cmd::Session(s, instances) => {
|
|
changed = true;
|
|
if (s.sample_rate.as_f64() - self.session.sample_rate.as_f64()).abs() > 0.5 {
|
|
let mut old = std::mem::replace(&mut self.mix, new_engine(s.sample_rate.as_f64() as f32));
|
|
let _ = self.garbage.send(old.drain_plugins());
|
|
}
|
|
self.mix.adopt(instances);
|
|
// The main format changed (a document edit, not steady state): resize.
|
|
let nch = main_channels(&s);
|
|
if self.pending.len() != nch {
|
|
self.pending = vec![vec![0.0; BLOCK]; nch];
|
|
self.pending_len = 0;
|
|
self.read = 0.0;
|
|
}
|
|
self.fold = fold_gains(&s);
|
|
let old = std::mem::replace(&mut self.session, s);
|
|
if let Some(tx) = &self.old_sessions {
|
|
let _ = tx.try_send(old);
|
|
}
|
|
}
|
|
Cmd::OldSessions(tx) => self.old_sessions = Some(tx),
|
|
Cmd::Play { from, end, looped } => {
|
|
self.pos = from;
|
|
self.end = end;
|
|
self.looped = looped.filter(|r| r.len() > 64);
|
|
self.playing = true;
|
|
self.pending_len = 0;
|
|
self.read = 0.0;
|
|
self.mix.reset();
|
|
}
|
|
Cmd::Stop => {
|
|
self.playing = false;
|
|
self.end = None;
|
|
self.looped = None;
|
|
self.mix.reset();
|
|
}
|
|
Cmd::Locate(at) => {
|
|
self.pos = at;
|
|
self.pending_len = 0;
|
|
self.read = 0.0;
|
|
self.mix.reset();
|
|
}
|
|
}
|
|
}
|
|
if changed {
|
|
// Place adopted instances now (not only once playback starts), so editors see them;
|
|
// `sync` creates no third-party plugins here.
|
|
self.mix.ensure_synced(&self.session);
|
|
}
|
|
self.send_garbage();
|
|
self.shared.playing.store(self.playing, Ordering::Relaxed);
|
|
}
|
|
|
|
fn send_garbage(&mut self) {
|
|
let g = self.mix.take_retired();
|
|
if !g.is_empty() {
|
|
let _ = self.garbage.send(g);
|
|
}
|
|
}
|
|
|
|
/// Render the next block at the session rate into `pending`.
|
|
fn render_next(&mut self, quantum: usize) {
|
|
let mut n = quantum.clamp(1, BLOCK);
|
|
if let Some(l) = self.looped
|
|
&& self.pos >= l.end
|
|
{
|
|
self.pos = l.start;
|
|
}
|
|
if let Some(l) = self.looped {
|
|
n = n.min(usize::try_from((l.end - self.pos).max(1)).unwrap_or(BLOCK));
|
|
} else if let Some(e) = self.end {
|
|
if self.pos >= e {
|
|
self.playing = false;
|
|
self.end = None;
|
|
self.shared.playing.store(false, Ordering::Relaxed);
|
|
n = 0;
|
|
} else {
|
|
n = n.min(usize::try_from(e - self.pos).unwrap_or(BLOCK));
|
|
}
|
|
}
|
|
if n == 0 {
|
|
for c in &mut self.pending {
|
|
c.iter_mut().for_each(|x| *x = 0.0);
|
|
}
|
|
self.pending_len = BLOCK;
|
|
return;
|
|
}
|
|
if let Some(ring) = &self.input {
|
|
let ch = ring.channels.load(Ordering::Relaxed).max(1);
|
|
if self.mix.input.len() != ch {
|
|
self.mix.input = vec![vec![0.0; BLOCK]; ch];
|
|
}
|
|
ring.pop_into(&mut self.mix.input, n, self.session.sample_rate.hz());
|
|
}
|
|
self.mix.monitor_only = !self.playing;
|
|
self.mix.recording = self.recording;
|
|
self.mix.render(&self.session, self.pos, n, &mut self.pending);
|
|
self.send_garbage();
|
|
self.pending_len = n;
|
|
self.publish_meters();
|
|
if self.playing {
|
|
self.pos += n as i64;
|
|
self.shared.position.store(self.pos, Ordering::Relaxed);
|
|
}
|
|
}
|
|
|
|
/// Drop deleted tracks' meters from the shared snapshot, unless the UI holds it right now.
|
|
fn forget_meters(&mut self) {
|
|
if !self.forget_shared.is_empty()
|
|
&& let Ok(mut m) = self.shared.meters.try_lock()
|
|
{
|
|
m.forget(&self.forget_shared);
|
|
self.forget_shared.clear();
|
|
}
|
|
}
|
|
|
|
/// Hand this block's meters to the UI without blocking: they are folded (max) into what the
|
|
/// UI has not taken yet. If the UI holds the snapshot right now, they wait in `unsent`.
|
|
fn publish_meters(&mut self) {
|
|
self.unsent.accumulate(&self.mix.main_meter, &self.mix.meters);
|
|
self.forget_meters();
|
|
if let Ok(mut m) = self.shared.meters.try_lock() {
|
|
m.accumulate(&self.unsent.main, &self.unsent.tracks);
|
|
self.unsent.reset_levels();
|
|
}
|
|
}
|
|
|
|
/// Fill an interleaved device buffer.
|
|
pub(crate) fn fill(&mut self, out: &mut [f32], channels: usize) {
|
|
self.drain();
|
|
let channels = channels.max(1);
|
|
let monitoring = self.input.is_some() && self.session.tracks.iter().any(|t| t.mixer.input_monitor || t.mixer.record_arm);
|
|
if !self.playing && !monitoring {
|
|
out.iter_mut().for_each(|x| *x = 0.0);
|
|
self.unsent.silence();
|
|
self.forget_meters();
|
|
if let Ok(mut m) = self.shared.meters.try_lock() {
|
|
m.silence();
|
|
}
|
|
return;
|
|
}
|
|
let speed = f64::from(self.shared.speed.load(Ordering::Relaxed)) / 1000.0;
|
|
let step = self.session.sample_rate.as_f64() / self.device_rate.max(1.0) * speed.clamp(0.1, 4.0);
|
|
let callback_frames = out.len() / channels;
|
|
let quantum = ((callback_frames as f64 * step).ceil() as usize).clamp(1, BLOCK);
|
|
for frame in out.chunks_mut(channels) {
|
|
while self.read >= self.pending_len as f64 {
|
|
self.read -= self.pending_len as f64;
|
|
if !self.playing && !monitoring {
|
|
break;
|
|
}
|
|
self.render_next(quantum);
|
|
if self.pending_len == 0 {
|
|
break;
|
|
}
|
|
}
|
|
let i0 = self.read.floor() as usize;
|
|
let fr = (self.read - i0 as f64) as f32;
|
|
let sounding = self.playing || monitoring;
|
|
let nch = self.pending.len();
|
|
let sample = |c: usize| {
|
|
let ch = self.pending.get(c);
|
|
let a = ch.and_then(|v| v.get(i0)).copied().unwrap_or(0.0);
|
|
// `pending` is BLOCK long; only `pending_len` frames are valid. Hold the last one at the edge.
|
|
let b = if i0 + 1 < self.pending_len { ch.and_then(|v| v.get(i0 + 1)).copied().unwrap_or(a) } else { a };
|
|
a * (1.0 - fr) + b * fr
|
|
};
|
|
write_frame(frame, channels, nch, sounding, &self.fold, sample);
|
|
self.read += step;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Write one device frame from `nch` rendered channels: fold surround to stereo (a mono device
|
|
/// averages the two sides), or copy the channels straight through. `sample(c)` is channel `c`.
|
|
fn write_frame(frame: &mut [f32], channels: usize, nch: usize, sounding: bool, fold: &[(f32, f32)], sample: impl Fn(usize) -> f32) {
|
|
if channels < nch {
|
|
// Fold surround to stereo first; a mono device averages the two sides.
|
|
let (mut l, mut r) = (0.0f32, 0.0f32);
|
|
if sounding {
|
|
if nch == 2 {
|
|
(l, r) = (sample(0), sample(1));
|
|
} else {
|
|
for (c, (gl, gr)) in fold.iter().enumerate().take(nch) {
|
|
let v = sample(c);
|
|
l += v * gl;
|
|
r += v * gr;
|
|
}
|
|
}
|
|
}
|
|
for (c, o) in frame.iter_mut().enumerate() {
|
|
*o = match c {
|
|
0 if channels == 1 => out_sample(0.5 * l + 0.5 * r),
|
|
0 => out_sample(l),
|
|
1 => out_sample(r),
|
|
_ => 0.0,
|
|
};
|
|
}
|
|
} else {
|
|
for (c, o) in frame.iter_mut().enumerate() {
|
|
*o = if c < nch.max(1) && sounding { out_sample(sample(c)) } else { 0.0 };
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Clamp to the device range, mapping NaN and infinities to silence.
|
|
fn out_sample(v: f32) -> f32 {
|
|
if v.is_finite() { v.clamp(-1.0, 1.0) } else { 0.0 }
|
|
}
|
|
|
|
/// UI-thread bookkeeping of the third-party instances the audio thread holds.
|
|
pub(crate) struct Side {
|
|
/// What was last created for each slot: (plugin id, channels, created ok).
|
|
pub(crate) shadow: HashMap<(TrackId, usize), (String, usize, bool)>,
|
|
/// Session sample rate the instances were prepared for.
|
|
rate: f32,
|
|
/// Editor handles of the live instances.
|
|
pub(crate) editors: HashMap<(TrackId, usize), Box<dyn PluginEditor>>,
|
|
/// State readers of the live instances.
|
|
pub(crate) states: BTreeMap<(TrackId, usize), Box<dyn PluginStateReader>>,
|
|
/// Instances that refused their stored state: saves keep that state until the plugin's editor
|
|
/// is opened.
|
|
pub(crate) refused: HashSet<(TrackId, usize)>,
|
|
garbage: Receiver<Vec<Box<dyn Plugin>>>,
|
|
old_sessions: Option<Receiver<Arc<Session>>>,
|
|
/// The tracks of the session last sent, to tell the audio thread which ones went.
|
|
tracks: HashSet<TrackId>,
|
|
}
|
|
|
|
impl Side {
|
|
pub(crate) fn new(rate: f32, garbage: Receiver<Vec<Box<dyn Plugin>>>) -> Side {
|
|
Side {
|
|
shadow: HashMap::new(),
|
|
rate,
|
|
editors: HashMap::new(),
|
|
states: BTreeMap::new(),
|
|
refused: HashSet::new(),
|
|
garbage,
|
|
old_sessions: None,
|
|
tracks: HashSet::new(),
|
|
}
|
|
}
|
|
|
|
/// Note `s`'s tracks; the ones the previous session had and `s` doesn't.
|
|
pub(crate) fn gone_tracks(&mut self, s: &Session) -> Vec<TrackId> {
|
|
let now: HashSet<TrackId> = s.tracks.iter().map(|t| t.id).collect();
|
|
let gone = self.tracks.difference(&now).copied().collect();
|
|
self.tracks = now;
|
|
gone
|
|
}
|
|
|
|
/// Drops retired instances (closing their editors first, inside the plugin's drop) and
|
|
/// replaced session snapshots.
|
|
pub(crate) fn collect_garbage(&mut self) {
|
|
while let Ok(g) = self.garbage.try_recv() {
|
|
for p in g {
|
|
log::debug!("plugin instance released: {}", p.info().name);
|
|
drop(p);
|
|
}
|
|
}
|
|
if let Some(rx) = &self.old_sessions {
|
|
rx.try_iter().for_each(drop);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn return_old_sessions(&mut self, tx: &Sender<Cmd>) {
|
|
let (otx, orx) = sync_channel(OLD_SESSIONS);
|
|
self.old_sessions = Some(orx);
|
|
let _ = tx.send(Cmd::OldSessions(otx));
|
|
}
|
|
|
|
pub(crate) fn forget(&mut self, key: (TrackId, usize)) {
|
|
self.shadow.remove(&key);
|
|
self.states.remove(&key);
|
|
self.refused.remove(&key);
|
|
if let Some(mut e) = self.editors.remove(&key) {
|
|
e.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn take_meter_snapshot(source: &mut MeterSnapshot, out: &mut MeterSnapshot) {
|
|
if !source.fresh {
|
|
// No block since the last take: keep the readings, but not those of deleted tracks.
|
|
out.tracks.retain(|id, _| source.tracks.contains_key(id));
|
|
return;
|
|
}
|
|
copy_meter_snapshot(source, out);
|
|
source.reset_levels();
|
|
}
|
|
|
|
pub(crate) fn copy_meter_snapshot(source: &MeterSnapshot, out: &mut MeterSnapshot) {
|
|
out.tracks.retain(|id, _| source.tracks.contains_key(id));
|
|
for (id, meter) in &source.tracks {
|
|
out.tracks.entry(*id).or_default().copy_from(meter);
|
|
}
|
|
out.main.copy_from(&source.main);
|
|
}
|
|
|
|
impl Side {
|
|
pub(crate) fn capture_states(&mut self) -> PluginStates {
|
|
self.states.iter_mut().filter(|(k, _)| !self.refused.contains(*k)).filter_map(|(&(t, slot), r)| Some((t, slot, r.save_state()?))).collect()
|
|
}
|
|
|
|
pub(crate) fn changed_states(&mut self) -> Vec<(TrackId, usize)> {
|
|
self.editors.iter_mut().filter_map(|(k, e)| e.state_changed().then_some(*k)).collect()
|
|
}
|
|
|
|
pub(crate) fn capture_states_of(&mut self, slots: &[(TrackId, usize)]) -> PluginStates {
|
|
self.states
|
|
.iter_mut()
|
|
.filter(|(k, _)| !self.refused.contains(*k) && slots.contains(k))
|
|
.filter_map(|(&(t, slot), r)| Some((t, slot, r.save_state()?)))
|
|
.collect()
|
|
}
|
|
|
|
/// A plugin that marks its state changed while it loads or restores it hasn't changed.
|
|
pub(crate) fn add_editor(&mut self, key: (TrackId, usize), mut e: Box<dyn PluginEditor>) {
|
|
let _ = e.state_changed();
|
|
self.editors.insert(key, e);
|
|
}
|
|
|
|
pub(crate) fn open_editor(&mut self, key: (TrackId, usize)) -> Result<(), String> {
|
|
let e = self.editors.get_mut(&key).ok_or_else(|| "this plugin has no editor (or it is not loaded)".to_string())?;
|
|
e.open()?;
|
|
self.refused.remove(&key);
|
|
Ok(())
|
|
}
|
|
|
|
/// Creates the third-party instances `s` needs that the audio thread does not have yet.
|
|
pub(crate) fn prepare(&mut self, s: &Session) -> Vec<PreparedInstance> {
|
|
self.collect_garbage();
|
|
let sr = s.sample_rate.as_f64() as f32;
|
|
if (sr - self.rate).abs() > 0.5 {
|
|
// The audio thread rebuilds its engine: everything is created anew.
|
|
let keys: Vec<(TrackId, usize)> = self.shadow.keys().copied().collect();
|
|
for k in keys {
|
|
self.forget(k);
|
|
}
|
|
self.rate = sr;
|
|
}
|
|
let specs = soundcraft_mix::instance_specs(s);
|
|
let stale: Vec<(TrackId, usize)> = self.shadow.keys().filter(|k| !specs.iter().any(|x| (x.track, x.slot) == **k)).copied().collect();
|
|
for k in stale {
|
|
self.forget(k);
|
|
}
|
|
let mut out = Vec::new();
|
|
for spec in specs {
|
|
let key = (spec.track, spec.slot);
|
|
if self.shadow.get(&key).is_some_and(|(id, ch, _)| *id == spec.id && *ch == spec.ch) {
|
|
continue;
|
|
}
|
|
self.forget(key);
|
|
let made = soundcraft_mix::create_instance(&spec.id, spec.state.as_deref(), sr, BLOCK, spec.ch);
|
|
log::debug!("plugin instance for {:?} slot {}: {} ({} ch, created: {})", spec.track, spec.slot, spec.id, spec.ch, made.is_some());
|
|
self.shadow.insert(key, (spec.id.clone(), spec.ch, made.is_some()));
|
|
if let Some(soundcraft_mix::NewInstance { mut plugin, refused_state }) = made {
|
|
if let Some(e) = plugin.editor() {
|
|
self.add_editor(key, e);
|
|
}
|
|
if let Some(r) = plugin.state_reader() {
|
|
self.states.insert(key, r);
|
|
}
|
|
if refused_state {
|
|
self.refused.insert(key);
|
|
}
|
|
out.push(PreparedInstance { track: spec.track, slot: spec.slot, id: spec.id, ch: spec.ch, plugin });
|
|
}
|
|
}
|
|
out
|
|
}
|
|
}
|
|
|
|
/// ITU stereo fold-down gains for each main channel of a session.
|
|
fn fold_gains(s: &Session) -> [(f32, f32); MAX_CHANNELS] {
|
|
let mut g = [(0.0, 0.0); MAX_CHANNELS];
|
|
let f = s.main_format();
|
|
let sp = f.speakers();
|
|
if sp.len() == f.channels() {
|
|
for (o, x) in g.iter_mut().zip(sp.iter()) {
|
|
*o = itu_stereo_fold(*x);
|
|
}
|
|
} else {
|
|
// Ambisonics: W only, at -3 dB per side.
|
|
g[0] = (std::f32::consts::FRAC_1_SQRT_2, std::f32::consts::FRAC_1_SQRT_2);
|
|
}
|
|
g
|
|
}
|