2021-11-23 08:19:29 +00:00
|
|
|
// Copyright (C) 2021 Tomasz Andrzejak <andreiltd@gmail.com>
|
|
|
|
//
|
2022-01-15 18:40:12 +00:00
|
|
|
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
|
|
|
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
|
|
|
// <https://mozilla.org/MPL/2.0/>.
|
|
|
|
//
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
2021-11-23 08:19:29 +00:00
|
|
|
|
|
|
|
use gst::prelude::*;
|
|
|
|
|
|
|
|
use anyhow::{bail, Error};
|
|
|
|
|
|
|
|
use std::sync::{Arc, Condvar, Mutex};
|
|
|
|
use std::{env, thread, time};
|
|
|
|
|
|
|
|
// Rotation in radians to apply to object position every 100 ms
|
|
|
|
const ROTATION: f32 = 2.0 / 180.0 * std::f32::consts::PI;
|
|
|
|
|
|
|
|
fn run() -> Result<(), Error> {
|
|
|
|
gst::init()?;
|
|
|
|
gstrsaudiofx::plugin_register_static()?;
|
|
|
|
|
|
|
|
let args: Vec<String> = env::args().collect();
|
|
|
|
|
|
|
|
// Ircam binaries that the hrtf plugin is using can be downloaded from:
|
|
|
|
// https://github.com/mrDIMAS/hrir_sphere_builder/tree/master/hrtf_base/IRCAM
|
|
|
|
//
|
|
|
|
// Ideally provide a mono input as this example is moving a single object. Otherwise
|
|
|
|
// the input will be downmixed to mono with the audioconvert element.
|
|
|
|
//
|
|
|
|
// e.g.: hrtfrender 'file:///path/to/my/awesome/mono/wav/awesome.wav' IRC_1002_C.bin
|
|
|
|
if args.len() != 3 {
|
|
|
|
bail!("Usage: {} URI HRIR", args[0].clone());
|
|
|
|
}
|
|
|
|
|
|
|
|
let uri = &args[1];
|
|
|
|
let hrir = &args[2];
|
|
|
|
|
|
|
|
let pipeline = gst::parse_launch(&format!(
|
|
|
|
"uridecodebin uri={} ! audioconvert ! audio/x-raw,channels=1 !
|
|
|
|
hrtfrender hrir-file={} name=hrtf ! audioresample ! autoaudiosink",
|
|
|
|
uri, hrir
|
|
|
|
))?
|
|
|
|
.downcast::<gst::Pipeline>()
|
|
|
|
.expect("type error");
|
|
|
|
|
|
|
|
let hrtf = pipeline.by_name("hrtf").expect("hrtf element not found");
|
|
|
|
|
|
|
|
// At the beginning put an object in front of listener
|
|
|
|
let objs = [gst::Structure::builder("application/spatial-object")
|
|
|
|
.field("x", 0f32)
|
|
|
|
.field("y", 0f32)
|
|
|
|
.field("z", 1f32)
|
|
|
|
.field("distance-gain", 1f32)
|
|
|
|
.build()];
|
|
|
|
|
|
|
|
hrtf.set_property("spatial-objects", gst::Array::new(objs));
|
|
|
|
|
|
|
|
let state_cond = Arc::new((Mutex::new(gst::State::Null), Condvar::new()));
|
|
|
|
let state_cond_clone = Arc::clone(&state_cond);
|
|
|
|
|
|
|
|
thread::spawn(move || {
|
|
|
|
// Wait for the pipeline to start up
|
|
|
|
{
|
|
|
|
let (lock, cvar) = &*state_cond_clone;
|
|
|
|
let mut state = lock.lock().unwrap();
|
|
|
|
|
|
|
|
while *state != gst::State::Playing {
|
|
|
|
state = cvar.wait(state).unwrap();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
loop {
|
|
|
|
// get current object position and rotate it clockwise
|
|
|
|
let s = hrtf.property::<gst::Array>("spatial-objects")[0]
|
|
|
|
.get::<gst::Structure>()
|
|
|
|
.expect("type error");
|
|
|
|
|
|
|
|
// positive values are on the right side of a listener
|
|
|
|
let x = s.get::<f32>("x").expect("type error");
|
|
|
|
// elevation, positive value is up
|
|
|
|
let y = s.get::<f32>("y").expect("type error");
|
|
|
|
// positive values are in front of a listener
|
|
|
|
let z = s.get::<f32>("z").expect("type error");
|
|
|
|
// gain
|
|
|
|
let gain = s.get::<f32>("distance-gain").expect("type error");
|
|
|
|
|
|
|
|
// rotate clockwise: https://en.wikipedia.org/wiki/Rotation_matrix
|
|
|
|
let new_x = x * f32::cos(ROTATION) + z * f32::sin(ROTATION);
|
|
|
|
let new_z = -x * f32::sin(ROTATION) + z * f32::cos(ROTATION);
|
|
|
|
|
|
|
|
let objs = [gst::Structure::builder("application/spatial-object")
|
2022-11-01 08:27:48 +00:00
|
|
|
.field("x", new_x)
|
|
|
|
.field("y", y)
|
|
|
|
.field("z", new_z)
|
|
|
|
.field("distance-gain", gain)
|
2021-11-23 08:19:29 +00:00
|
|
|
.build()];
|
|
|
|
|
|
|
|
hrtf.set_property("spatial-objects", gst::Array::new(objs));
|
|
|
|
|
|
|
|
thread::sleep(time::Duration::from_millis(100));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
pipeline.set_state(gst::State::Playing)?;
|
|
|
|
|
|
|
|
let bus = pipeline.bus().unwrap();
|
|
|
|
for msg in bus.iter_timed(gst::ClockTime::NONE) {
|
|
|
|
use gst::MessageView;
|
|
|
|
|
|
|
|
match msg.view() {
|
|
|
|
MessageView::StateChanged(state_changed) => {
|
|
|
|
if state_changed.src().map(|s| s == pipeline).unwrap_or(false)
|
|
|
|
&& state_changed.current() == gst::State::Playing
|
|
|
|
{
|
|
|
|
let (lock, cvar) = &*state_cond;
|
|
|
|
let mut state = lock.lock().unwrap();
|
|
|
|
|
|
|
|
*state = gst::State::Playing;
|
|
|
|
cvar.notify_one();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
MessageView::Eos(..) => break,
|
|
|
|
MessageView::Error(err) => {
|
|
|
|
println!(
|
|
|
|
"Error from {:?}: {} ({:?})",
|
|
|
|
msg.src().map(|s| s.path_string()),
|
|
|
|
err.error(),
|
|
|
|
err.debug()
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pipeline.set_state(gst::State::Null)?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
match run() {
|
|
|
|
Ok(r) => r,
|
|
|
|
Err(e) => eprintln!("Error! {}", e),
|
|
|
|
}
|
|
|
|
}
|