WIP: Preparing the codebase, nothing important
Some checks failed
ci/woodpecker/push/code_tests Pipeline failed
ci/woodpecker/push/pre_commit_test Pipeline failed

This commit is contained in:
2025-11-22 20:31:38 +01:00
committed by Nikolai Rodionov
parent 213c5b1a47
commit 87ee669502
35 changed files with 2830 additions and 10 deletions

2
.codespellrc Normal file
View File

@@ -0,0 +1,2 @@
[codespell]
ignore-words-list = ratatui

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
resources/** filter=lfs diff=lfs merge=lfs -text

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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]

View File

@@ -1,2 +1,8 @@
# termix
[![status-badge](https://ci.badhouseplants.net/api/badges/19/status.svg)](https://ci.badhouseplants.net/repos/19)
# Requirenments
On all systems:
- protoc

22
engine/Cargo.toml Normal file
View 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"]

View 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!()
}
}

View 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!()
}
}

View 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")
}
}

View 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>>;
}

View 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!()
}
}

View 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))
}
}

View 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
View 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(())
}

View File

@@ -0,0 +1,7 @@
[package]
name = "jack-playback"
version = "0.1.0"
edition = "2024"
[dependencies]
jack = "0.13.3"

View File

@@ -0,0 +1,5 @@
fn main() {
// 1. Create client
let (client, _status) =
jack::Client::new("rust_jack_simple", jack::ClientOptions::default()).unwrap();
}

View 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"

View File

@@ -0,0 +1 @@
Set this on mac export DYLD_FALLBACK_LIBRARY_PATH="$(brew --prefix jack)/lib:${DYLD_FALLBACK_LIBRARY_PATH:-}"

View 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
View 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
View 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(())
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

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

Binary file not shown.

View File

@@ -1,3 +0,0 @@
fn main() {
println!("Hello, world!");
}

10
tui/Cargo.toml Normal file
View 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
View 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());
}