8 Commits

Author SHA1 Message Date
824939672b Another pointless stuff
Some checks failed
ci/woodpecker/push/code_tests Pipeline failed
ci/woodpecker/push/pre_commit_test Pipeline was successful
2025-12-27 20:54:30 +01:00
2ebf5d1ea2 Some sounds are already produced
Some checks failed
ci/woodpecker/push/code_tests Pipeline failed
ci/woodpecker/push/pre_commit_test Pipeline was successful
2025-12-24 14:02:36 +01:00
a827afd872 Trying to get into jack
Some checks failed
ci/woodpecker/push/code_tests Pipeline failed
ci/woodpecker/push/pre_commit_test Pipeline was successful
2025-12-24 13:57:56 +01:00
535727268d Trying to get into jack
Some checks failed
ci/woodpecker/push/code_tests Pipeline failed
ci/woodpecker/push/pre_commit_test Pipeline was successful
2025-12-24 13:30:15 +01:00
29181f9c76 Update plugins
Some checks failed
ci/woodpecker/push/code_tests Pipeline failed
ci/woodpecker/push/pre_commit_test Pipeline was successful
2025-12-23 19:26:36 +01:00
0a043e10f1 Nothig
Some checks failed
ci/woodpecker/push/code_tests Pipeline failed
ci/woodpecker/push/pre_commit_test Pipeline was successful
2025-12-22 09:45:40 +01:00
6bdb38bcee Something stupid
Some checks failed
ci/woodpecker/push/pre_commit_test Pipeline was successful
ci/woodpecker/push/code_tests Pipeline failed
2025-12-18 17:33:00 +01:00
Nikolai Rodionov
49e4f05b5e Try starting monolith
Some checks failed
ci/woodpecker/push/code_tests Pipeline failed
ci/woodpecker/push/pre_commit_test Pipeline was successful
2025-12-18 16:03:54 +01:00
35 changed files with 846 additions and 2039 deletions

View File

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

1
.gitattributes vendored
View File

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

View File

@@ -1,10 +1,9 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
rev: v2.3.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

1956
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,23 @@
[workspace]
resolver = "3"
members = ["engine", "examples/jack-playback", "examples/jack-sine", "lib", "tui"]
[package]
name = "termix"
version = "0.1.0"
edition = "2024"
[workspace.dependencies]
[dependencies]
clap = { version = "4.5.53", features = ["derive"] }
jack = { version = "0.13.3" }
pulseaudio = { version = "0.3.1", optional = true}
alsa = { version = "0.10.0", optional = true}
symphonia = { version = "0.5.5", features = ["mp3"] }
coreaudio-rs = { version = "0.13.0", optional = true }
cpal = { version = "0.16.0", optional = true }
ringbuf = "0.4.8"
crossbeam = { version = "0.8.4", features = ["crossbeam-channel"] }
crossbeam-channel = "0.5.15"
anyhow = "1.0.100"
[features]
pulseaudio = ["dep:pulseaudio"]
alsa = ["dep:alsa"]
coreaudio = ["dep:coreaudio-rs"]
cpal = ["dep:cpal"]

View File

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

View File

@@ -1,22 +0,0 @@
[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

@@ -1,26 +0,0 @@
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

@@ -1,19 +0,0 @@
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

@@ -1,75 +0,0 @@
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

@@ -1,17 +0,0 @@
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

@@ -1,9 +0,0 @@
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

@@ -1,125 +0,0 @@
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

@@ -1,6 +0,0 @@
pub(crate) mod dummy;
pub(crate) mod grpc;
pub(crate) trait ControlPane {
async fn start_server(&self) -> Result<(), Box<dyn std::error::Error>>;
}

View File

@@ -1,29 +0,0 @@
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

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

View File

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

View File

@@ -1,8 +0,0 @@
[package]
name = "jack-sine"
version = "0.1.0"
edition = "2024"
[dependencies]
crossbeam-channel = "0.5.15"
jack = "0.13.3"

View File

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

View File

@@ -1,100 +0,0 @@
//! 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,
}
}

View File

@@ -1,16 +0,0 @@
[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"

View File

@@ -1,29 +0,0 @@
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

@@ -1,49 +0,0 @@
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;
}

View File

@@ -1,19 +0,0 @@
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;
}

View File

@@ -1,7 +0,0 @@
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");
}
}

View File

@@ -1,25 +0,0 @@
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;
}
}

View File

@@ -1,9 +0,0 @@
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,
}

View File

@@ -1,11 +0,0 @@
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,
}

View File

@@ -1,14 +0,0 @@
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)

Binary file not shown.

BIN
resources/audio/session.mp3 (Stored with Git LFS)

Binary file not shown.

BIN
resources/audio/session.wav (Stored with Git LFS)

Binary file not shown.

213
src/main.rs Normal file
View File

@@ -0,0 +1,213 @@
//! Sine wave generator with frequency configuration exposed through standard
//! input.
use crossbeam_channel::bounded;
use jack::{AsyncClient, AudioOut, Client, ProcessHandler, ProcessScope};
use ringbuf::traits::{Consumer, Producer, Split};
use ringbuf::HeapRb;
use std::fs::File;
use std::str::FromStr;
use std::thread::{self, sleep};
use std::time::Duration;
use std::{env, io};
use symphonia::core::audio::{AudioBufferRef, SampleBuffer, Signal};
use symphonia::core::codecs::DecoderOptions;
use symphonia::core::formats::FormatOptions;
use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;
use symphonia::core::units::Time;
const RB_SIZE: usize = 48000 * 2; // ~1 second mono
//
//
fn start_decoder_thread(
mut looper: AudioFileLooper,
mut producer: impl ringbuf::traits::Producer<Item = f32> + std::marker::Send + 'static,
) {
thread::spawn(move || {
loop {
let sample = looper.next_sample();
let _ = producer.try_push(sample);
// Drop if buffer is full (never block)
}
});
}
//
//
struct JackHandler<C>
where
C: Consumer<Item = f32>,
{
out: jack::Port<jack::AudioOut>,
consumer: C,
}
impl<C> ProcessHandler for JackHandler<C>
where
C: Consumer<Item = f32> + std::marker::Send,
{
fn process(&mut self, _: &Client, ps: &ProcessScope) -> jack::Control {
let buffer = self.out.as_mut_slice(ps);
for sample in buffer.iter_mut() {
*sample = self.consumer.try_pop().unwrap_or(0.0);
}
jack::Control::Continue
}
}
fn main() {
let path = env::args()
.nth(1)
.expect("Usage: cargo run -- <audio_file>");
let mut looper = AudioFileLooper::open(&path).expect("Brah");
// Ring buffer
let rb = HeapRb::<f32>::new(RB_SIZE);
let (producer, consumer) = rb.split();
// Start decoder thread
start_decoder_thread(looper, producer);
// 1. open a client
let (client, _status) =
jack::Client::new("rust_jack_sine", jack::ClientOptions::default()).unwrap();
// 2. register port
let out = client
.register_port("sine_out", jack::AudioOut::default())
.unwrap();
let handler = JackHandler { out, consumer };
let active_client = {
let notification_handler = ();
AsyncClient::new(client, notification_handler, handler)
}
.expect("bvrah");
println!("Running… press Enter to quit");
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
// 4. Activate the client. Also connect the ports to the system audio.
// 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,
}
}
pub struct AudioFileLooper {
format: Box<dyn symphonia::core::formats::FormatReader>,
decoder: Box<dyn symphonia::core::codecs::Decoder>,
track_id: u32,
sample_buf: Vec<f32>,
sample_pos: usize,
}
impl AudioFileLooper {
pub fn open(path: &str) -> Result<Self, symphonia::core::errors::Error> {
let file = File::open(path)?;
let mss = MediaSourceStream::new(Box::new(file), Default::default());
let mut hint = Hint::new();
if let Some(ext) = path.split('.').last() {
hint.with_extension(ext);
}
let format = symphonia::default::get_probe()
.format(
&hint,
mss,
&FormatOptions::default(),
&MetadataOptions::default(),
)?
.format;
let track = format
.tracks()
.iter()
.find(|t| t.codec_params.sample_rate.is_some())
.unwrap();
let track_id = track.id;
let decoder = symphonia::default::get_codecs()
.make(&track.codec_params, &DecoderOptions::default())?;
Ok(Self {
format,
decoder,
track_id: track_id,
sample_buf: Vec::new(),
sample_pos: 0,
})
}
fn refill_samples(&mut self) -> Result<(), symphonia::core::errors::Error> {
self.sample_buf.clear();
self.sample_pos = 0;
loop {
match self.format.next_packet() {
Ok(packet) => {
if packet.track_id() != self.track_id {
continue;
}
let decoded = self.decoder.decode(&packet)?;
match decoded {
AudioBufferRef::F32(buf) => {
self.sample_buf.extend_from_slice(buf.chan(0));
}
_ => {
let mut sample_buffer = SampleBuffer::<f32>::new(
decoded.capacity() as u64,
*decoded.spec(),
);
sample_buffer.copy_interleaved_ref(decoded);
self.sample_buf.extend_from_slice(sample_buffer.samples());
}
}
return Ok(());
}
Err(symphonia::core::errors::Error::IoError(_)) => {
// EOF → loop
self.format.seek(
symphonia::core::formats::SeekMode::Accurate,
symphonia::core::formats::SeekTo::Time {
time: Time::new(0, 0.0),
track_id: Some(self.track_id),
},
)?;
}
Err(e) => return Err(e),
}
}
}
pub fn next_sample(&mut self) -> f32 {
if self.sample_pos >= self.sample_buf.len() {
if self.refill_samples().is_err() {
return 0.0;
}
}
let sample = self.sample_buf[self.sample_pos];
self.sample_pos += 1;
sample
}
}

View File

@@ -1,10 +0,0 @@
[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"

View File

@@ -1,31 +0,0 @@
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());
}