Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
cb0c92f567 |
16
Cargo.toml
16
Cargo.toml
@ -1,14 +1,2 @@
|
||||
[package]
|
||||
name = "arona"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
cpal = { version = "*", features = ["asio-sys", "asio", "jack"] }
|
||||
iced = "*"
|
||||
rayon = "*"
|
||||
crossbeam = "*"
|
||||
serde = { version = "*", features = ["derive"] }
|
||||
serde_json = "*"
|
||||
[workspace]
|
||||
members = ["src/daw", "src/host"]
|
||||
|
@ -1,147 +0,0 @@
|
||||
mod audio_processor;
|
||||
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct AudioConfig {
|
||||
sample_rate: Option<u32>,
|
||||
buffer_size: Option<u32>,
|
||||
input_device: Option<String>,
|
||||
output_device: Option<String>,
|
||||
}
|
||||
|
||||
impl AudioConfig {
|
||||
fn load() -> Self {
|
||||
let config = std::fs::read_to_string("audio_config.json");
|
||||
if config.is_err() {
|
||||
AudioConfig {
|
||||
sample_rate: None,
|
||||
buffer_size: None,
|
||||
input_device: None,
|
||||
output_device: None,
|
||||
}
|
||||
}
|
||||
else {
|
||||
let result = serde_json::from_str(&config.unwrap());
|
||||
if result.is_err() {
|
||||
println!("failed to parse audio_config.json");
|
||||
AudioConfig {
|
||||
sample_rate: None,
|
||||
buffer_size: None,
|
||||
input_device: None,
|
||||
output_device: None,
|
||||
}
|
||||
}
|
||||
else {
|
||||
result.unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save(&self) {
|
||||
let config = serde_json::to_string(&self).expect("failed to serialize audio_config");
|
||||
std::fs::write("audio_config.json", config).expect("failed to write audio_config.json");
|
||||
}
|
||||
fn get_sample_rate(&self) -> cpal::SampleRate {
|
||||
cpal::SampleRate(self.sample_rate.unwrap_or(48000))
|
||||
}
|
||||
fn get_buffer_size(&self) -> cpal::BufferSize {
|
||||
cpal::BufferSize::Fixed(self.buffer_size.unwrap_or(512))
|
||||
}
|
||||
fn get_input_device(&self, default: cpal::Device) -> cpal::Device {
|
||||
let host = cpal::default_host();
|
||||
let input_device = self.input_device.as_ref().map(|name| {
|
||||
host.devices().unwrap().find(|device| {
|
||||
device.name().unwrap() == *name
|
||||
})
|
||||
}).flatten();
|
||||
input_device.unwrap_or(default)
|
||||
}
|
||||
fn get_output_device(&self, default: cpal::Device) -> cpal::Device {
|
||||
let host = cpal::default_host();
|
||||
let output_device = self.output_device.as_ref().map(|name| {
|
||||
host.devices().unwrap().find(|device| {
|
||||
device.name().unwrap() == *name
|
||||
})
|
||||
}).flatten();
|
||||
output_device.unwrap_or(default)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AudioEngine {
|
||||
host : cpal::Host,
|
||||
input_device: cpal::Device,
|
||||
output_device: cpal::Device,
|
||||
sample_rate: cpal::SampleRate,
|
||||
buffer_size: cpal::BufferSize,
|
||||
|
||||
input_stream: Option<cpal::Stream>,
|
||||
output_stream: Option<cpal::Stream>,
|
||||
}
|
||||
|
||||
impl AudioEngine {
|
||||
pub fn new() -> Self {
|
||||
println!("Supported hosts:\n {:?}", cpal::ALL_HOSTS);
|
||||
let default_host = cpal::default_host();
|
||||
let default_input_device = default_host.default_input_device().expect("no input device");
|
||||
let default_output_device = default_host.default_output_device().expect("no output device");
|
||||
|
||||
let audio_config = AudioConfig::load();
|
||||
|
||||
let out = AudioEngine {
|
||||
host: default_host,
|
||||
input_device: audio_config.get_input_device(default_input_device),
|
||||
output_device: audio_config.get_output_device(default_output_device),
|
||||
sample_rate: audio_config.get_sample_rate(),
|
||||
buffer_size: audio_config.get_buffer_size(),
|
||||
input_stream: None,
|
||||
output_stream: None,
|
||||
};
|
||||
out.save_config();
|
||||
println!("Input device: {}", out.input_device.name().unwrap());
|
||||
println!("Output device: {}", out.output_device.name().unwrap());
|
||||
out
|
||||
}
|
||||
|
||||
pub fn save_config(&self) {
|
||||
let audio_config = AudioConfig {
|
||||
sample_rate: Some(self.sample_rate.0),
|
||||
buffer_size: Some(match self.buffer_size {
|
||||
cpal::BufferSize::Default => { 0 }
|
||||
cpal::BufferSize::Fixed(size) => { size }
|
||||
}),
|
||||
input_device: Some(self.input_device.name().unwrap()),
|
||||
output_device: Some(self.output_device.name().unwrap()),
|
||||
};
|
||||
audio_config.save();
|
||||
}
|
||||
|
||||
pub fn open_stream(&mut self) {
|
||||
// 不要创建输入流,输入流只在需要时创建
|
||||
let output_stream = self.output_device.build_output_stream(
|
||||
&self.output_stream_config(),
|
||||
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
|
||||
self.process_output(data);
|
||||
},
|
||||
move |err| {
|
||||
eprintln!("an error occurred on stream: {}", err);
|
||||
},
|
||||
None
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
fn output_stream_config(&self) -> cpal::StreamConfig {
|
||||
cpal::StreamConfig {
|
||||
channels: 2,
|
||||
buffer_size: self.buffer_size,
|
||||
sample_rate: self.sample_rate,
|
||||
}
|
||||
}
|
||||
|
||||
fn process_output(&mut self, data: &mut [f32]) {
|
||||
for sample in data.iter_mut() {
|
||||
*sample = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
15
src/daw/Cargo.toml
Normal file
15
src/daw/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "arona"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
cpal = { version = "*", features = ["asio-sys", "asio", "jack"] }
|
||||
iced = "*"
|
||||
rayon = "*"
|
||||
crossbeam = "*"
|
||||
serde = { version = "*", features = ["derive"] }
|
||||
serde_json = "*"
|
||||
arona_host = { path = "../host" }
|
6
src/daw/src/audio_engine/audio_device.rs
Normal file
6
src/daw/src/audio_engine/audio_device.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use cpal::traits::DeviceTrait;
|
||||
|
||||
pub fn get_device_name(device: Option<&cpal::Device>) -> String {
|
||||
device.map(|device| device.name().unwrap()).unwrap_or("None".to_string())
|
||||
}
|
||||
|
195
src/daw/src/audio_engine/mod.rs
Normal file
195
src/daw/src/audio_engine/mod.rs
Normal file
@ -0,0 +1,195 @@
|
||||
pub mod audio_processor;
|
||||
pub mod audio_device;
|
||||
|
||||
use cpal::SupportedBufferSize;
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::audio_engine::audio_device::get_device_name;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct AudioConfig {
|
||||
sample_rate: Option<u32>,
|
||||
buffer_size: Option<u32>,
|
||||
input_device: Option<String>,
|
||||
output_device: Option<String>,
|
||||
}
|
||||
|
||||
impl AudioConfig {
|
||||
fn load() -> Self {
|
||||
let config = std::fs::read_to_string("audio_config.json");
|
||||
let default = AudioConfig {
|
||||
sample_rate: None,
|
||||
buffer_size: None,
|
||||
input_device: None,
|
||||
output_device: None,
|
||||
};
|
||||
if config.is_err() {
|
||||
println!("读取audio_config.json失败, 将会使用默认配置");
|
||||
return default;
|
||||
}
|
||||
let result = serde_json::from_str(&config.unwrap());
|
||||
if result.is_err() {
|
||||
println!("解析audio_config.json失败, 将会使用默认配置");
|
||||
}
|
||||
result.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn save(&self) {
|
||||
let config = serde_json::to_string(&self).expect("序列化audio_config.json失败");
|
||||
std::fs::write("audio_config.json", config).expect("无法写入audio_config.json");
|
||||
}
|
||||
fn get_sample_rate(&self, default: u32) -> cpal::SampleRate {
|
||||
cpal::SampleRate(self.sample_rate.unwrap_or(default))
|
||||
}
|
||||
fn get_buffer_size(&self, default: cpal::BufferSize) -> cpal::BufferSize {
|
||||
let size = self.buffer_size.unwrap_or(0);
|
||||
if size == 0 {
|
||||
default
|
||||
} else {
|
||||
cpal::BufferSize::Fixed(size)
|
||||
}
|
||||
}
|
||||
fn get_input_device(&self, default: Option<cpal::Device>) -> Option<cpal::Device> {
|
||||
let host = cpal::default_host();
|
||||
let input_device = self.input_device.as_ref().map(|name| {
|
||||
host.devices().unwrap().find(|device| {
|
||||
device.name().unwrap() == *name
|
||||
})
|
||||
}).flatten();
|
||||
input_device.or(default)
|
||||
}
|
||||
fn get_output_device(&self, default: Option<cpal::Device>) -> Option<cpal::Device> {
|
||||
let host = cpal::default_host();
|
||||
let output_device = self.output_device.as_ref().map(|name| {
|
||||
host.devices().unwrap().find(|device| {
|
||||
device.name().unwrap() == *name
|
||||
})
|
||||
}).flatten();
|
||||
output_device.or(default)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AudioEngine {
|
||||
host : cpal::Host,
|
||||
input_device: Option<cpal::Device>,
|
||||
output_device: Option<cpal::Device>,
|
||||
sample_rate: cpal::SampleRate,
|
||||
buffer_size: cpal::BufferSize,
|
||||
|
||||
input_stream: Option<cpal::Stream>,
|
||||
output_stream: Option<cpal::Stream>,
|
||||
}
|
||||
|
||||
impl AudioEngine {
|
||||
pub fn new() -> Self {
|
||||
println!("Supported hosts:\n {:?}", cpal::ALL_HOSTS);
|
||||
|
||||
let default_host = cpal::host_from_id(cpal::HostId::Asio).unwrap_or(cpal::default_host());
|
||||
let default_output_device = default_host.default_output_device();
|
||||
let output_config = default_output_device.as_ref().unwrap().default_output_config().unwrap();
|
||||
let sample_rate = output_config.sample_rate();
|
||||
|
||||
let audio_config = AudioConfig::load();
|
||||
|
||||
let out = AudioEngine {
|
||||
host: default_host,
|
||||
input_device: audio_config.get_input_device(None),
|
||||
output_device: audio_config.get_output_device(default_output_device),
|
||||
sample_rate: audio_config.get_sample_rate(sample_rate.0),
|
||||
buffer_size: audio_config.get_buffer_size(cpal::BufferSize::Default),
|
||||
input_stream: None,
|
||||
output_stream: None,
|
||||
};
|
||||
out.save_config();
|
||||
|
||||
let input_device_name = get_device_name(out.input_device.as_ref());
|
||||
let output_device_name = get_device_name(out.output_device.as_ref());
|
||||
println!("Using API: {}", out.host.id().name());
|
||||
println!("Input device: {}", input_device_name);
|
||||
println!("Output device: {}", output_device_name);
|
||||
out
|
||||
}
|
||||
|
||||
pub fn save_config(&self) {
|
||||
let audio_config = AudioConfig {
|
||||
sample_rate: Some(self.sample_rate.0),
|
||||
buffer_size: Some(match self.buffer_size {
|
||||
cpal::BufferSize::Default => { 0 }
|
||||
cpal::BufferSize::Fixed(size) => { size }
|
||||
}),
|
||||
input_device: Some(get_device_name(self.input_device.as_ref())),
|
||||
output_device: Some(get_device_name(self.output_device.as_ref())),
|
||||
};
|
||||
audio_config.save();
|
||||
}
|
||||
|
||||
pub fn open_stream(&mut self) {
|
||||
self.try_open_input_stream();
|
||||
self.try_open_output_stream();
|
||||
}
|
||||
|
||||
fn try_open_output_stream(&mut self) {
|
||||
if self.output_device.is_none() {
|
||||
return;
|
||||
}
|
||||
let device = self.output_device.as_ref().unwrap();
|
||||
let config = self.output_stream_config();
|
||||
let output_stream = device.build_output_stream(
|
||||
&config,
|
||||
|data, callback| process_output(data, callback),
|
||||
move |err| eprintln!("an error occurred on stream: {}", err),
|
||||
None
|
||||
);
|
||||
if output_stream.is_err() {
|
||||
eprintln!("failed to open output stream: {}", output_stream.err().unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
self.output_stream = Some(output_stream.unwrap());
|
||||
let play_result = self.output_stream.as_mut().unwrap().play();
|
||||
if play_result.is_err() {
|
||||
eprintln!("failed to play output stream: {}", play_result.err().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
fn try_open_input_stream(&mut self) {
|
||||
if self.input_device.is_none() {
|
||||
return;
|
||||
}
|
||||
let device = self.input_device.as_ref().unwrap();
|
||||
let input_stream = device.build_input_stream(
|
||||
&self.input_stream_config(),
|
||||
|data, callback| process_input(data, callback),
|
||||
move |err| eprintln!("an error occurred on stream: {}", err),
|
||||
None
|
||||
).unwrap();
|
||||
input_stream.play().unwrap();
|
||||
self.input_stream = Some(input_stream);
|
||||
}
|
||||
|
||||
fn output_stream_config(&self) -> cpal::StreamConfig {
|
||||
cpal::StreamConfig {
|
||||
channels: 2,
|
||||
buffer_size: self.buffer_size,
|
||||
sample_rate: self.sample_rate,
|
||||
}
|
||||
}
|
||||
|
||||
fn input_stream_config(&self) -> cpal::StreamConfig {
|
||||
cpal::StreamConfig {
|
||||
channels: 2,
|
||||
buffer_size: self.buffer_size,
|
||||
sample_rate: self.sample_rate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_output(data: &mut [f32], callback_info: &cpal::OutputCallbackInfo) {
|
||||
for sample in data.iter_mut() {
|
||||
*sample = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
fn process_input(data: &[f32], callback_info: &cpal::InputCallbackInfo) {
|
||||
|
||||
}
|
12
src/daw/src/main.rs
Normal file
12
src/daw/src/main.rs
Normal file
@ -0,0 +1,12 @@
|
||||
mod audio_engine;
|
||||
|
||||
use std::thread::sleep;
|
||||
use audio_engine::*;
|
||||
|
||||
fn main() {
|
||||
let mut engine = AudioEngine::new();
|
||||
engine.open_stream();
|
||||
loop {
|
||||
sleep(std::time::Duration::from_millis(1));
|
||||
}
|
||||
}
|
17
src/host/Cargo.toml
Normal file
17
src/host/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "arona_host"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
vst2 = "*"
|
||||
shmem-ipc = "*"
|
||||
sdl2 = "*"
|
||||
|
||||
[[bin]]
|
||||
name = "vst2host"
|
||||
path = "src/vst2host.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "vst3host"
|
||||
path = "src/vst3host.rs"
|
35
src/host/src/host.rs
Normal file
35
src/host/src/host.rs
Normal file
@ -0,0 +1,35 @@
|
||||
|
||||
pub enum HostEvent<'a> {
|
||||
Midi {
|
||||
data: [u8; 3],
|
||||
delta_frames: i32,
|
||||
live: bool,
|
||||
note_length: Option<i32>,
|
||||
note_offset: Option<i32>,
|
||||
detune: i8,
|
||||
note_off_velocity: u8,
|
||||
},
|
||||
SysEx {
|
||||
payload: &'a [u8],
|
||||
delta_frames: i32,
|
||||
},
|
||||
}
|
||||
|
||||
pub trait HostInterface {
|
||||
fn get_host_name(&self) -> String;
|
||||
fn get_host_version(&self) -> String;
|
||||
fn get_host_vendor(&self) -> String;
|
||||
|
||||
|
||||
fn set_parameter(&mut self, index: i32, value: f32);
|
||||
fn get_parameter(&self, index: i32) -> f32;
|
||||
|
||||
fn process_f32(&mut self, input: Vec<&mut [f32]>, output: Vec<&mut [f32]>);
|
||||
fn process_f64(&mut self, input: Vec<&mut [f64]>, output: Vec<&mut [f64]>);
|
||||
fn process_events(&mut self, events: Vec<HostEvent>);
|
||||
|
||||
fn open_editor(&mut self);
|
||||
fn close_editor(&mut self);
|
||||
fn is_editor_open(&self) -> bool;
|
||||
fn has_editor(&mut self) -> bool;
|
||||
}
|
1
src/host/src/lib.rs
Normal file
1
src/host/src/lib.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod host;
|
148
src/host/src/vst2host.rs
Normal file
148
src/host/src/vst2host.rs
Normal file
@ -0,0 +1,148 @@
|
||||
extern crate vst2;
|
||||
|
||||
mod host;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use vst2::buffer::AudioBuffer;
|
||||
use vst2::host::*;
|
||||
use vst2::plugin::*;
|
||||
use vst2::event::Event;
|
||||
use crate::host::HostEvent;
|
||||
use host::HostInterface;
|
||||
|
||||
impl HostEvent {
|
||||
fn to_vst2_event(&self) -> Event {
|
||||
match self {
|
||||
HostEvent::Midi { data, delta_frames, live, note_length, note_offset, detune, note_off_velocity } => {
|
||||
Event::Midi {
|
||||
data: *data,
|
||||
delta_frames: *delta_frames,
|
||||
live: *live,
|
||||
note_length: note_length.map(|x| *x),
|
||||
note_offset: note_offset.map(|x| *x),
|
||||
detune: *detune,
|
||||
note_off_velocity: *note_off_velocity,
|
||||
}
|
||||
},
|
||||
HostEvent::SysEx { payload, delta_frames } => {
|
||||
Event::SysEx {
|
||||
payload,
|
||||
delta_frames: *delta_frames,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Vst2Host { on_automate: Option<Box<dyn Fn(i32, f32) + Send>> }
|
||||
|
||||
impl Host for Vst2Host {
|
||||
fn automate(&self, index: i32, value: f32) {
|
||||
self.on_automate.as_ref().map(|f| f(index, value));
|
||||
}
|
||||
}
|
||||
|
||||
struct InternalVst2Host {
|
||||
host: Arc<Mutex<Vst2Host>>,
|
||||
loader: PluginLoader<Vst2Host>,
|
||||
instance: PluginInstance,
|
||||
window: Option<Window>,
|
||||
}
|
||||
|
||||
fn on_window_update(window: &mut Window, elapsed: std::time::Duration) {
|
||||
|
||||
}
|
||||
|
||||
impl InternalVst2Host {
|
||||
fn new(path: &std::path::Path) -> Self {
|
||||
let host = Arc::new(Mutex::new(Vst2Host{
|
||||
on_automate: None,
|
||||
}));
|
||||
|
||||
let mut loader = PluginLoader::load(path, host.clone()).unwrap();
|
||||
let mut instance = loader.instance().unwrap();
|
||||
let editor = instance.get_editor();
|
||||
|
||||
let mut window : Option<Window> = None;
|
||||
if editor.is_some() {
|
||||
let w = Window::new(instance.get_info().name.as_mut(), on_window_update);
|
||||
let editor_size = editor.unwrap().size();
|
||||
window = Some(w);
|
||||
}
|
||||
|
||||
InternalVst2Host {
|
||||
host,
|
||||
loader,
|
||||
instance,
|
||||
window,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_automate(&self, index: i32, value: f32) {
|
||||
println!("Automate: {} {}", index, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl HostInterface for InternalVst2Host {
|
||||
fn get_host_name(&self) -> String {
|
||||
self.instance.get_info().name
|
||||
}
|
||||
|
||||
fn get_host_version(&self) -> String {
|
||||
self.instance.get_info().version.to_string()
|
||||
}
|
||||
|
||||
fn get_host_vendor(&self) -> String {
|
||||
self.instance.get_info().vendor
|
||||
}
|
||||
|
||||
fn set_parameter(&mut self, index: i32, value: f32) {
|
||||
self.instance.set_parameter(index, value);
|
||||
}
|
||||
|
||||
fn get_parameter(&self, index: i32) -> f32 {
|
||||
self.instance.get_parameter(index)
|
||||
}
|
||||
|
||||
fn process_f32(&mut self, input: Vec<&mut [f32]>, output: Vec<&mut [f32]>) {
|
||||
let buffer = AudioBuffer::new(input, output);
|
||||
self.instance.process(buffer);
|
||||
}
|
||||
|
||||
fn process_f64(&mut self, input: Vec<&mut [f64]>, output: Vec<&mut [f64]>) {
|
||||
let buffer = AudioBuffer::new(input, output);
|
||||
self.instance.process_f64(buffer);
|
||||
}
|
||||
|
||||
fn process_events(&mut self, events: Vec<HostEvent>) {
|
||||
let events = events.iter().map(|event| event.to_vst2_event()).collect();
|
||||
self.instance.process_events(events);
|
||||
}
|
||||
|
||||
fn open_editor(&mut self) {
|
||||
let editor = self.instance.get_editor();
|
||||
editor.unwrap().open();
|
||||
}
|
||||
|
||||
fn close_editor(&mut self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn is_editor_open(&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn has_editor(&mut self) -> bool {
|
||||
self.instance.get_editor().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// 获取参数
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
// 参数1是插件路径
|
||||
let path = std::path::Path::new(&args[1]);
|
||||
let internal_host = InternalVst2Host::new(path);
|
||||
|
||||
let mut shmem = Sheme;
|
||||
}
|
5
src/host/src/vst3host.rs
Normal file
5
src/host/src/vst3host.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod host;
|
||||
|
||||
fn main() {
|
||||
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
mod audio_engine;
|
||||
use audio_engine::*;
|
||||
|
||||
fn main() {
|
||||
let mut engine = AudioEngine::new();
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user