WIP: Preparing the codebase, nothing important
This commit is contained in:
2
.codespellrc
Normal file
2
.codespellrc
Normal file
@@ -0,0 +1,2 @@
|
||||
[codespell]
|
||||
ignore-words-list = ratatui
|
||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
resources/** filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -1,9 +1,10 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v2.3.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-merge-conflict
|
||||
- id: check-toml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
|
||||
2138
Cargo.lock
generated
2138
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
[package]
|
||||
name = "termix"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["engine", "examples/jack-playback", "examples/jack-sine", "lib", "tui"]
|
||||
|
||||
[dependencies]
|
||||
[workspace.dependencies]
|
||||
|
||||
@@ -1,2 +1,8 @@
|
||||
# termix
|
||||
[](https://ci.badhouseplants.net/repos/19)
|
||||
|
||||
|
||||
# Requirenments
|
||||
|
||||
On all systems:
|
||||
- protoc
|
||||
|
||||
22
engine/Cargo.toml
Normal file
22
engine/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "engine"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
coreaudio-rs = { version = "0.13.0", optional = true }
|
||||
crossbeam-channel = "0.5.15"
|
||||
env_logger = "0.11.8"
|
||||
jack = {version = "0.13.3", optional = true }
|
||||
lib = { path = "../lib/" }
|
||||
log = "0.4.28"
|
||||
prost = "0.14.1"
|
||||
tokio = { version = "1.48.0", features = ["rt-multi-thread"] }
|
||||
tonic = "0.14.2"
|
||||
tonic-prost = "0.14.2"
|
||||
tonic-reflection = "0.14.2"
|
||||
|
||||
[features]
|
||||
jack = ["dep:jack"]
|
||||
coreaudio = ["dep:coreaudio-rs"]
|
||||
26
engine/src/audio_engine/coreaudio_ab.rs
Normal file
26
engine/src/audio_engine/coreaudio_ab.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use crate::audio_engine::AudioBackend;
|
||||
|
||||
pub(crate) struct CoreAudioBackend {}
|
||||
|
||||
impl CoreAudioBackend {}
|
||||
|
||||
#[cfg(feature = "coreaudio")]
|
||||
impl AudioBackend for CoreAudioBackend {
|
||||
fn start_client(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn describe_backend() -> Result<super::BackendDescription, Box<dyn std::error::Error>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "coreaudio"))]
|
||||
impl AudioBackend for CoreAudioBackend {
|
||||
fn start_client(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
todo!()
|
||||
}
|
||||
fn describe_backend() -> Result<super::BackendDescription, Box<dyn std::error::Error>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
19
engine/src/audio_engine/dummy_ab.rs
Normal file
19
engine/src/audio_engine/dummy_ab.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use crate::audio_engine::AudioBackend;
|
||||
|
||||
struct DummyAudioBackend {}
|
||||
|
||||
impl DummyAudioBackend {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioBackend for DummyAudioBackend {
|
||||
fn start_client(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn describe_backend() -> Result<super::BackendDescription, Box<dyn std::error::Error>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
75
engine/src/audio_engine/jack_ab.rs
Normal file
75
engine/src/audio_engine/jack_ab.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use crate::audio_engine::AudioBackend;
|
||||
use crossbeam_channel::bounded;
|
||||
use log::{info, warn};
|
||||
use std::error::Error;
|
||||
|
||||
#[cfg(feature = "jack")]
|
||||
use jack;
|
||||
#[cfg(feature = "jack")]
|
||||
use jack::ClientOptions;
|
||||
|
||||
pub(crate) struct JackAudioBackend {
|
||||
pub(crate) feature_jack: bool,
|
||||
pub(crate) running: bool,
|
||||
status: JackStatus,
|
||||
}
|
||||
|
||||
impl JackAudioBackend {
|
||||
pub(crate) fn new() -> Self {
|
||||
let feature_jack = cfg!(feature = "jack");
|
||||
// TODO: It should be retrieved from the system
|
||||
let running = true;
|
||||
let status = JackStatus::default();
|
||||
Self {
|
||||
feature_jack,
|
||||
running,
|
||||
status,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "jack")]
|
||||
#[derive(Default)]
|
||||
pub(crate) struct JackStatus {
|
||||
client: Option<jack::Client>,
|
||||
status: Option<jack::ClientStatus>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "jack")]
|
||||
impl AudioBackend for JackAudioBackend {
|
||||
fn start_client(&mut self) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Get the possible input and output ports to use them for the session initialization
|
||||
fn describe_backend() -> Result<super::BackendDescription, Box<dyn Error>> {
|
||||
use jack::{Client, PortFlags};
|
||||
use crate::audio_engine::BackendDescription;
|
||||
|
||||
let (client, _) = Client::new("list_ports", ClientOptions::empty())?;
|
||||
let ports_in = client.ports(None, None, PortFlags::IS_INPUT);
|
||||
let ports_out = client.ports(None, None, PortFlags::IS_OUTPUT);
|
||||
let output = BackendDescription {
|
||||
audio_devices_out: ports_out,
|
||||
audio_devices_in: ports_in,
|
||||
};
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(not(feature = "jack"))]
|
||||
#[derive(Default)]
|
||||
pub(crate) struct JackStatus {}
|
||||
|
||||
#[cfg(not(feature = "jack"))]
|
||||
impl AudioBackend for JackAudioBackend {
|
||||
fn start_client(&mut self) -> Result<(), Box<dyn Error>> {
|
||||
warn!("jack support is not enabled");
|
||||
Ok(())
|
||||
}
|
||||
fn describe_backend() -> Result<super::BackendDescription, Box<dyn Error>> {
|
||||
unimplemented!("jack is disabled")
|
||||
}
|
||||
}
|
||||
17
engine/src/audio_engine/mod.rs
Normal file
17
engine/src/audio_engine/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use std::error::Error;
|
||||
|
||||
pub(crate) mod coreaudio_ab;
|
||||
pub(crate) mod dummy_ab;
|
||||
pub(crate) mod jack_ab;
|
||||
|
||||
pub(crate) struct BackendDescription {
|
||||
pub(crate) audio_devices_out: Vec<String>,
|
||||
pub(crate) audio_devices_in: Vec<String>,
|
||||
}
|
||||
pub(crate) trait AudioBackend {
|
||||
// Start a audio backend client
|
||||
// It should be executed either on the startup,
|
||||
// or when the audio backend is switched
|
||||
fn start_client(&mut self) -> Result<(), Box<dyn Error>>;
|
||||
fn describe_backend() -> Result<BackendDescription, Box<dyn Error>>;
|
||||
}
|
||||
9
engine/src/control_pane/dummy.rs
Normal file
9
engine/src/control_pane/dummy.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use crate::control_pane::ControlPane;
|
||||
|
||||
struct Dummy {}
|
||||
|
||||
impl ControlPane for Dummy {
|
||||
async fn start_server(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
125
engine/src/control_pane/grpc/mod.rs
Normal file
125
engine/src/control_pane/grpc/mod.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use std::result::Result;
|
||||
|
||||
use lib::termix::{
|
||||
self,
|
||||
audio_backend::{
|
||||
AudioBackendDescription, Backend, BackendList, DesiredAudioBacked, FILE_DESCRIPTOR_SET,
|
||||
SupportedAudioBackends,
|
||||
audio_backend_rpc_server::{AudioBackendRpc, AudioBackendRpcServer},
|
||||
},
|
||||
};
|
||||
use log::info;
|
||||
use tonic::{Response, Status, transport::Server};
|
||||
use tonic_reflection::server;
|
||||
|
||||
use crate::{
|
||||
audio_engine::{
|
||||
AudioBackend,
|
||||
jack_ab::{self, JackAudioBackend},
|
||||
},
|
||||
control_pane::ControlPane,
|
||||
};
|
||||
|
||||
pub(crate) struct Grpc {
|
||||
pub(crate) port: i32,
|
||||
pub(crate) enable_reflections: bool,
|
||||
}
|
||||
|
||||
impl ControlPane for Grpc {
|
||||
async fn start_server(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
info!("starting the grpc server on port {}", self.port);
|
||||
// TODO: Use the port from self
|
||||
let addr = "[::1]:50051".parse()?;
|
||||
let mut server = Server::builder();
|
||||
let audio_backend_reflections = server::Builder::configure()
|
||||
.register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET)
|
||||
.build_v1()
|
||||
.unwrap();
|
||||
let audio_backend = TermixAudioBackend::default();
|
||||
|
||||
server
|
||||
.add_service(audio_backend_reflections)
|
||||
.add_service(AudioBackendRpcServer::new(audio_backend))
|
||||
.serve(addr)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TermixAudioBackend {}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl AudioBackendRpc for TermixAudioBackend {
|
||||
async fn stop_client(
|
||||
&self,
|
||||
_: tonic::Request<()>,
|
||||
) -> Result<tonic::Response<()>, tonic::Status> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn start_client(
|
||||
&self,
|
||||
request: tonic::Request<DesiredAudioBacked>,
|
||||
) -> Result<tonic::Response<()>, tonic::Status> {
|
||||
info!("starting the audio backend client");
|
||||
match request.get_ref().backend() {
|
||||
SupportedAudioBackends::AbUnspecified => {
|
||||
unimplemented!("unsupported backend");
|
||||
}
|
||||
SupportedAudioBackends::AbJack => {
|
||||
info!("trying to use JACK as the backend");
|
||||
let mut ab = jack_ab::JackAudioBackend::new();
|
||||
ab.start_client();
|
||||
}
|
||||
SupportedAudioBackends::AbCoreaudio => todo!(),
|
||||
};
|
||||
Ok(Response::new(()))
|
||||
}
|
||||
|
||||
async fn describe_backend(
|
||||
&self,
|
||||
request: tonic::Request<DesiredAudioBacked>,
|
||||
) -> Result<tonic::Response<AudioBackendDescription>, tonic::Status> {
|
||||
info!("Describing the audio backend");
|
||||
match request.get_ref().backend() {
|
||||
SupportedAudioBackends::AbUnspecified => return Err(Status::not_found("backend os not specified")),
|
||||
SupportedAudioBackends::AbCoreaudio => todo!(),
|
||||
SupportedAudioBackends::AbJack => {
|
||||
let backend_desc= match jack_ab::JackAudioBackend::describe_backend(){
|
||||
Ok(desc) => desc,
|
||||
Err(err) => return Err(Status::internal(err.to_string())),
|
||||
};
|
||||
Ok(Response::new(AudioBackendDescription{
|
||||
core_audio_description: None,
|
||||
input_devices: backend_desc.audio_devices_in,
|
||||
output_devices: backend_desc.audio_devices_out
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
async fn init_connection(
|
||||
&self,
|
||||
request: tonic::Request<()>,
|
||||
) -> Result<tonic::Response<()>, tonic::Status> {
|
||||
info!("initializing the connection to the audio backend");
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_available_backends(
|
||||
&self,
|
||||
_: tonic::Request<()>,
|
||||
) -> Result<Response<BackendList>, tonic::Status> {
|
||||
info!("discovering available backends");
|
||||
|
||||
let mut response = BackendList::default();
|
||||
let jack = JackAudioBackend::new();
|
||||
if jack.feature_jack {
|
||||
response.backends.push(Backend {
|
||||
name: "jack".to_string(),
|
||||
});
|
||||
}
|
||||
Ok(Response::new(response))
|
||||
}
|
||||
}
|
||||
6
engine/src/control_pane/mod.rs
Normal file
6
engine/src/control_pane/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub(crate) mod dummy;
|
||||
pub(crate) mod grpc;
|
||||
|
||||
pub(crate) trait ControlPane {
|
||||
async fn start_server(&self) -> Result<(), Box<dyn std::error::Error>>;
|
||||
}
|
||||
29
engine/src/main.rs
Normal file
29
engine/src/main.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
pub(crate) mod audio_engine;
|
||||
pub(crate) mod control_pane;
|
||||
|
||||
use crate::control_pane::ControlPane;
|
||||
use clap::Parser;
|
||||
|
||||
/// Simple program to greet a person
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Args {
|
||||
#[arg(long, default_value_t = 50051)]
|
||||
grpc_port: i32,
|
||||
#[arg(long, default_value_t = true)]
|
||||
grpc_enable_reflections: bool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::init();
|
||||
let args = Args::parse();
|
||||
|
||||
let grpc_control_pane = control_pane::grpc::Grpc {
|
||||
port: args.grpc_port,
|
||||
enable_reflections: args.grpc_enable_reflections,
|
||||
};
|
||||
grpc_control_pane.start_server().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
7
examples/jack-playback/Cargo.toml
Normal file
7
examples/jack-playback/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "jack-playback"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
jack = "0.13.3"
|
||||
5
examples/jack-playback/src/main.rs
Normal file
5
examples/jack-playback/src/main.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
fn main() {
|
||||
// 1. Create client
|
||||
let (client, _status) =
|
||||
jack::Client::new("rust_jack_simple", jack::ClientOptions::default()).unwrap();
|
||||
}
|
||||
8
examples/jack-sine/Cargo.toml
Normal file
8
examples/jack-sine/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "jack-sine"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
crossbeam-channel = "0.5.15"
|
||||
jack = "0.13.3"
|
||||
1
examples/jack-sine/README.md
Normal file
1
examples/jack-sine/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Set this on mac ❯ export DYLD_FALLBACK_LIBRARY_PATH="$(brew --prefix jack)/lib:${DYLD_FALLBACK_LIBRARY_PATH:-}"
|
||||
100
examples/jack-sine/src/main.rs
Normal file
100
examples/jack-sine/src/main.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
//! Sine wave generator with frequency configuration exposed through standard
|
||||
//! input.
|
||||
|
||||
use crossbeam_channel::bounded;
|
||||
use jack::{PortFlags, PortSpec};
|
||||
use std::io;
|
||||
use std::str::FromStr;
|
||||
|
||||
fn main() {
|
||||
// 1. open a client
|
||||
let (client, _status) =
|
||||
jack::Client::new("rust_jack_sine", jack::ClientOptions::default()).unwrap();
|
||||
let ports = client.ports(None, Some(jack::AudioIn::default().jack_port_type()), PortFlags::empty());
|
||||
println!("All JACK ports:");
|
||||
for port in ports {
|
||||
println!("{}", port);
|
||||
}
|
||||
|
||||
// 2. register port
|
||||
let out_port = client
|
||||
.register_port("sine_out", jack::AudioOut::default())
|
||||
.unwrap();
|
||||
|
||||
// 3. define process callback handler
|
||||
let (tx, rx) = bounded(1_000_000);
|
||||
struct State {
|
||||
out_port: jack::Port<jack::AudioOut>,
|
||||
rx: crossbeam_channel::Receiver<f64>,
|
||||
frequency: f64,
|
||||
frame_t: f64,
|
||||
time: f64,
|
||||
}
|
||||
let process = jack::contrib::ClosureProcessHandler::with_state(
|
||||
State {
|
||||
out_port,
|
||||
rx,
|
||||
frequency: 220.0,
|
||||
frame_t: 1.0 / client.sample_rate() as f64,
|
||||
time: 0.0,
|
||||
},
|
||||
|state, _, ps| -> jack::Control {
|
||||
// Get output buffer
|
||||
let out = state.out_port.as_mut_slice(ps);
|
||||
|
||||
// Check frequency requests
|
||||
while let Ok(f) = state.rx.try_recv() {
|
||||
state.time = 0.0;
|
||||
state.frequency = f;
|
||||
}
|
||||
|
||||
// Write output
|
||||
for v in out.iter_mut() {
|
||||
let x = state.frequency * state.time * 2.0 * std::f64::consts::PI;
|
||||
let y = x.sin();
|
||||
*v = y as f32;
|
||||
state.time += state.frame_t;
|
||||
}
|
||||
|
||||
// Continue as normal
|
||||
jack::Control::Continue
|
||||
},
|
||||
move |_, _, _| jack::Control::Continue,
|
||||
);
|
||||
|
||||
// 4. Activate the client. Also connect the ports to the system audio.
|
||||
let active_client = client.activate_async((), process).unwrap();
|
||||
active_client
|
||||
.as_client()
|
||||
.connect_ports_by_name("rust_jack_sine:sine_out", "system:playback_1")
|
||||
.unwrap();
|
||||
active_client
|
||||
.as_client()
|
||||
.connect_ports_by_name("rust_jack_sine:sine_out", "system:playback_2")
|
||||
.unwrap();
|
||||
// processing starts here
|
||||
|
||||
// 5. wait or do some processing while your handler is running in real time.
|
||||
println!("Enter an integer value to change the frequency of the sine wave.");
|
||||
while let Some(f) = read_freq() {
|
||||
tx.send(f).unwrap();
|
||||
}
|
||||
|
||||
// 6. Optional deactivate. Not required since active_client will deactivate on
|
||||
// drop, though explicit deactivate may help you identify errors in
|
||||
// deactivate.
|
||||
if let Err(err) = active_client.deactivate() {
|
||||
eprintln!("JACK exited with error: {err}");
|
||||
};
|
||||
}
|
||||
|
||||
/// Attempt to read a frequency from standard in. Will block until there is
|
||||
/// user input. `None` is returned if there was an error reading from standard
|
||||
/// in, or the retrieved string wasn't a compatible u16 integer.
|
||||
fn read_freq() -> Option<f64> {
|
||||
let mut user_input = String::new();
|
||||
match io::stdin().read_line(&mut user_input) {
|
||||
Ok(_) => u16::from_str(user_input.trim()).ok().map(|n| n as f64),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
16
lib/Cargo.toml
Normal file
16
lib/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "lib"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
prost = "0.14.1"
|
||||
tokio = { version = "1.48.0", features = ["rt-multi-thread"] }
|
||||
tonic = "0.14.2"
|
||||
tonic-prost = "0.14.2"
|
||||
uuid = { version = "1.18.1", features = ["v4"] }
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.14.1"
|
||||
prost-types = "0.14.1"
|
||||
tonic-prost-build = "0.14.2"
|
||||
29
lib/build.rs
Normal file
29
lib/build.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use std::{env, fs, io::Result, path::PathBuf};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
|
||||
//let proto_dir = "proto";
|
||||
//let paths = fs::read_dir(proto_dir).unwrap();
|
||||
//for path in paths {
|
||||
// let path_str = path.unwrap().path().to_str().unwrap().to_string();
|
||||
// let descriptor = format!("{}_descriptor.bin", path_str);
|
||||
// tonic_prost_build::configure()
|
||||
// .file_descriptor_set_path(out_dir.join("audio_backend_descriptor.bin"))
|
||||
// .compile_protos(&["proto/audio_backend.proto"], &["proto"])
|
||||
// .unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
|
||||
//}
|
||||
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
tonic_prost_build::configure()
|
||||
.file_descriptor_set_path(out_dir.join("audio_backend_descriptor.bin"))
|
||||
.compile_protos(&["proto/audio_backend.proto"], &["proto"])
|
||||
.unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
|
||||
|
||||
tonic_prost_build::configure()
|
||||
.file_descriptor_set_path(out_dir.join("track_descriptor.bin"))
|
||||
.compile_protos(&["proto/track.proto"], &["proto"])
|
||||
.unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
49
lib/proto/audio_backend.proto
Normal file
49
lib/proto/audio_backend.proto
Normal file
@@ -0,0 +1,49 @@
|
||||
syntax = "proto3";
|
||||
package termix.audio_backend;
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
service AudioBackendRPC {
|
||||
// Stop the active audio server
|
||||
rpc StopClient(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
// Start the audio server of choice
|
||||
rpc StartClient(DesiredAudioBacked) returns (google.protobuf.Empty);
|
||||
// Get information about the possible audio backend configuration options
|
||||
rpc DescribeBackend(DesiredAudioBacked) returns (AudioBackendDescription);
|
||||
rpc InitConnection(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
rpc GetAvailableBackends(google.protobuf.Empty) returns (BackendList);
|
||||
}
|
||||
|
||||
enum SupportedAudioBackends {
|
||||
AB_UNSPECIFIED = 0;
|
||||
AB_JACK = 1;
|
||||
AB_COREAUDIO = 2;
|
||||
}
|
||||
|
||||
message DesiredAudioBacked {
|
||||
SupportedAudioBackends backend = 1;
|
||||
CoreAudioOptions core_audio_opts = 2;
|
||||
}
|
||||
|
||||
message AudioBackendDescription {
|
||||
CoreAudioAvailableOptions core_audio_description = 1;
|
||||
repeated string input_devices = 2;
|
||||
repeated string output_devices = 3;
|
||||
}
|
||||
|
||||
message CoreAudioAvailableOptions {
|
||||
repeated string input_devices = 1;
|
||||
repeated string output_devices = 2;
|
||||
}
|
||||
|
||||
message CoreAudioOptions {
|
||||
string input_device = 1;
|
||||
}
|
||||
|
||||
message BackendList {
|
||||
repeated Backend backends = 1;
|
||||
}
|
||||
|
||||
message Backend {
|
||||
string name = 1;
|
||||
}
|
||||
19
lib/proto/track.proto
Normal file
19
lib/proto/track.proto
Normal file
@@ -0,0 +1,19 @@
|
||||
syntax = "proto3";
|
||||
package termix.track;
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
service TrackOp {
|
||||
rpc Create(Track) returns (google.protobuf.Empty);
|
||||
rpc List(google.protobuf.Empty) returns (Tracks);
|
||||
}
|
||||
|
||||
message Track {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message Tracks {
|
||||
repeated Track tracks = 1;
|
||||
}
|
||||
|
||||
|
||||
7
lib/src/lib.rs
Normal file
7
lib/src/lib.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod termix {
|
||||
pub mod audio_backend {
|
||||
pub const FILE_DESCRIPTOR_SET: &[u8] =
|
||||
tonic::include_file_descriptor_set!("audio_backend_descriptor");
|
||||
tonic::include_proto!("termix.audio_backend");
|
||||
}
|
||||
}
|
||||
25
lib/src/metadata.rs
Normal file
25
lib/src/metadata.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct Metadata {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
pub fn new(name: String) -> Self {
|
||||
let id = Uuid::new_v4();
|
||||
Self { id, name }
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Uuid {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn set_name(&mut self, name: String) {
|
||||
self.name = name;
|
||||
}
|
||||
}
|
||||
9
lib/src/project.rs
Normal file
9
lib/src/project.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use crate::{metadata::Metadata, region::Region, track::Track};
|
||||
|
||||
pub struct Project {
|
||||
pub name: String,
|
||||
pub tracks: Option<Vec<Track>>,
|
||||
pub regions: Option<Vec<Region>>,
|
||||
// Current playhead position
|
||||
pub current_sample: u64,
|
||||
}
|
||||
11
lib/src/region.rs
Normal file
11
lib/src/region.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use crate::metadata::Metadata;
|
||||
|
||||
pub struct Region {
|
||||
pub metadata: Metadata,
|
||||
// Position of the track on the track
|
||||
pub starts_at: u64,
|
||||
// From which point of the audio source the region starts
|
||||
pub plays_from: u64,
|
||||
// Duration of the region after plays_from
|
||||
pub duration: u64,
|
||||
}
|
||||
14
lib/src/track.rs
Normal file
14
lib/src/track.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use crate::metadata::Metadata;
|
||||
|
||||
pub enum TrackType {
|
||||
Audio,
|
||||
Midi,
|
||||
}
|
||||
|
||||
pub struct Track {
|
||||
pub metadata: Metadata,
|
||||
pub track_type: TrackType,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
pub struct TrackStatus {}
|
||||
BIN
resources/audio/session.flac
(Stored with Git LFS)
Normal file
BIN
resources/audio/session.flac
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/audio/session.mp3
(Stored with Git LFS)
Normal file
BIN
resources/audio/session.mp3
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
resources/audio/session.wav
(Stored with Git LFS)
Normal file
BIN
resources/audio/session.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
10
tui/Cargo.toml
Normal file
10
tui/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "tui"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
color-eyre = "0.6.5"
|
||||
crossterm = "0.29.0"
|
||||
ratatui = "0.29.0"
|
||||
31
tui/src/main.rs
Normal file
31
tui/src/main.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, Event};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
|
||||
/// Simple program to greet a person
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Args {}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let _ = Args::parse();
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
if matches!(event::read()?, Event::Key(_)) {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame) {
|
||||
frame.render_widget("hello world", frame.area());
|
||||
}
|
||||
Reference in New Issue
Block a user