mirror of
https://github.com/ivabus/lonelyradio
synced 2024-11-10 02:25:17 +03:00
v0.2: Metadata support, samplerate removal
Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
This commit is contained in:
parent
c6b9bdcf54
commit
6e3327aa1f
32
Cargo.lock
generated
32
Cargo.lock
generated
|
@ -289,15 +289,6 @@ version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
|
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cmake"
|
|
||||||
version = "0.1.50"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
@ -572,15 +563,6 @@ dependencies = [
|
||||||
"windows-targets 0.52.4",
|
"windows-targets 0.52.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libsamplerate-sys"
|
|
||||||
version = "0.1.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "28853b399f78f8281cd88d333b54a63170c4275f6faea66726a2bea5cca72e0d"
|
|
||||||
dependencies = [
|
|
||||||
"cmake",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lofty"
|
name = "lofty"
|
||||||
version = "0.18.2"
|
version = "0.18.2"
|
||||||
|
@ -622,7 +604,6 @@ dependencies = [
|
||||||
"lofty",
|
"lofty",
|
||||||
"rand",
|
"rand",
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
"samplerate",
|
|
||||||
"serde",
|
"serde",
|
||||||
"symphonia",
|
"symphonia",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -683,10 +664,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "monolib"
|
name = "monolib"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
|
"rmp-serde",
|
||||||
"rodio",
|
"rodio",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1030,15 +1013,6 @@ dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "samplerate"
|
|
||||||
version = "0.2.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e032b2b24715c4f982f483ea3abdb3c9ba444d9f63e87b2843d6f998f5ba2698"
|
|
||||||
dependencies = [
|
|
||||||
"libsamplerate-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.197"
|
version = "1.0.197"
|
||||||
|
|
|
@ -27,7 +27,7 @@ symphonia = { version = "0.5.4", features = [
|
||||||
"all-formats",
|
"all-formats",
|
||||||
"opt-simd",
|
"opt-simd",
|
||||||
] }
|
] }
|
||||||
samplerate = "0.2.4"
|
#samplerate = "0.2.4"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
rmp-serde = "1.1.2"
|
rmp-serde = "1.1.2"
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> TCP radio for singles
|
> TCP radio for singles
|
||||||
|
|
||||||
Radio that uses unencrypted TCP socket for broadcasting raw PCM (16/44.1/LE) stream
|
Radio that uses unencrypted TCP socket for broadcasting tagged audio data.
|
||||||
|
|
||||||
Decodes audio streams using [symphonia](https://github.com/pdeljanov/Symphonia).
|
Decodes audio streams using [symphonia](https://github.com/pdeljanov/Symphonia).
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ All files (recursively) will be shuffled and played back. Public log will be dis
|
||||||
|
|
||||||
### Clients
|
### Clients
|
||||||
|
|
||||||
[monoclient](./monoclient) with optional channel separation, hardcoded input (16/44.1/LE).
|
[monoclient](./monoclient) is a recommended client for lonelyradio
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
monoclient <SERVER>:<PORT>
|
monoclient <SERVER>:<PORT>
|
||||||
|
@ -30,7 +30,7 @@ monoclient <SERVER>:<PORT>
|
||||||
|
|
||||||
### Other clients
|
### Other clients
|
||||||
|
|
||||||
SwiftUI client is availible in [platform](./platform) directory (not yet adapted for lonelyradio 0.2).
|
SwiftUI client is availible in [platform](./platform) directory.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
@ -3,24 +3,19 @@ use clap::Parser;
|
||||||
use rodio::buffer::SamplesBuffer;
|
use rodio::buffer::SamplesBuffer;
|
||||||
use rodio::{OutputStream, Sink};
|
use rodio::{OutputStream, Sink};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::io::{Read, Write};
|
use std::io::{IsTerminal, Read, Write};
|
||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
|
|
||||||
// How many samples to cache before playing in samples (both channels) SHOULD BE EVEN
|
// How many samples to cache before playing in samples (both channels) SHOULD BE EVEN
|
||||||
const BUFFER_SIZE: usize = 2400;
|
const BUFFER_SIZE: usize = 4800;
|
||||||
// How many buffers to cache
|
// How many buffers to cache
|
||||||
const CACHE_SIZE: usize = 100;
|
const CACHE_SIZE: usize = 10;
|
||||||
|
|
||||||
enum Channel {
|
|
||||||
Right,
|
|
||||||
Left,
|
|
||||||
Stereo,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct SentMetadata {
|
struct SentMetadata {
|
||||||
// In bytes, we need to read next track metadata
|
// In bytes, we need to read next track metadata
|
||||||
lenght: u64,
|
lenght: u64,
|
||||||
|
sample_rate: u32,
|
||||||
title: String,
|
title: String,
|
||||||
album: String,
|
album: String,
|
||||||
artist: String,
|
artist: String,
|
||||||
|
@ -30,91 +25,75 @@ struct SentMetadata {
|
||||||
struct Args {
|
struct Args {
|
||||||
/// Remote address
|
/// Remote address
|
||||||
address: String,
|
address: String,
|
||||||
#[arg(short, long, default_value = "s")]
|
|
||||||
/// L, R or S for Left, Right or Stereo
|
|
||||||
channel: String,
|
|
||||||
#[arg(short)]
|
|
||||||
/// Play only on specified channel, with it if channel = Right => L=0 and R=R, without L=R and R=R. No effect on Stereo
|
|
||||||
single: bool,
|
|
||||||
|
|
||||||
/// Do not erase previously played track from stdout
|
/// Do not use backspace control char
|
||||||
#[arg(short)]
|
#[arg(short)]
|
||||||
no_backspace: bool,
|
no_backspace: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn delete_chars(n: usize) {
|
||||||
let args = Args::parse();
|
print!("{}{}{}", "\u{8}".repeat(n), " ".repeat(n), "\u{8}".repeat(n));
|
||||||
let mut stream = TcpStream::connect(args.address).unwrap();
|
std::io::stdout().flush().expect("Failed to flush stdout")
|
||||||
println!("Connected to {} from {}", stream.peer_addr().unwrap(), stream.local_addr().unwrap());
|
}
|
||||||
|
|
||||||
let channel = match args.channel.to_ascii_lowercase().as_str() {
|
fn main() {
|
||||||
"l" => Channel::Left,
|
let mut args = Args::parse();
|
||||||
"r" => Channel::Right,
|
args.no_backspace |= !std::io::stdout().is_terminal();
|
||||||
"s" => Channel::Stereo,
|
let mut stream = TcpStream::connect(&args.address)
|
||||||
_ => panic!("Wrong channel specified"),
|
.unwrap_or_else(|err| panic!("Failed to connect to {}: {}", args.address, err.to_string()));
|
||||||
};
|
println!("Connected to {} from {}", stream.peer_addr().unwrap(), stream.local_addr().unwrap());
|
||||||
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
|
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
|
||||||
let sink = Sink::try_new(&stream_handle).unwrap();
|
let sink = Sink::try_new(&stream_handle).unwrap();
|
||||||
let mut buffer = [0u8; 4];
|
let mut buffer = [0u8; 2];
|
||||||
let mut samples = [0f32; BUFFER_SIZE];
|
let mut samples = [0f32; BUFFER_SIZE];
|
||||||
let mut latest_msg_len = 0;
|
let mut latest_msg_len = 0;
|
||||||
print!("Playing: ");
|
print!("Playing: ");
|
||||||
loop {
|
loop {
|
||||||
let mut index = 0usize;
|
let mut index = 0usize;
|
||||||
|
|
||||||
let md: SentMetadata = rmp_serde::from_read(&stream).unwrap();
|
let md: SentMetadata =
|
||||||
let seconds = md.lenght / (2 * 44100);
|
rmp_serde::from_read(&stream).expect("Failed to parse track metadata");
|
||||||
let message = format!(
|
let seconds = md.lenght / (2 * md.sample_rate as u64);
|
||||||
"{} - {} - {} ({}:{:02})",
|
let total_lenght = format!("{}:{:02}", seconds / 60, seconds % 60);
|
||||||
md.artist,
|
let message = format!("{} - {} - {} ", md.artist, md.album, md.title);
|
||||||
md.album,
|
|
||||||
md.title,
|
|
||||||
seconds / 60,
|
|
||||||
seconds % 60
|
|
||||||
);
|
|
||||||
if latest_msg_len != 0 {
|
if latest_msg_len != 0 {
|
||||||
if args.no_backspace {
|
if args.no_backspace {
|
||||||
print!("\nPlaying: ");
|
print!("\nPlaying: ");
|
||||||
} else {
|
} else {
|
||||||
print!("{}", "\u{8}".repeat(latest_msg_len));
|
delete_chars(latest_msg_len)
|
||||||
print!("{}", " ".repeat(latest_msg_len));
|
|
||||||
print!("{}", "\u{8}".repeat(latest_msg_len));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
print!("{}", message);
|
print!("{}", message);
|
||||||
std::io::stdout().flush().unwrap();
|
let mut prev_timestamp_len = 0;
|
||||||
|
if args.no_backspace {
|
||||||
|
print!("({})", &total_lenght)
|
||||||
|
} else {
|
||||||
|
print!("(0:00 / {})", &total_lenght);
|
||||||
|
// (0:00/ + :00 + minutes len
|
||||||
|
prev_timestamp_len = 12 + format!("{}", seconds / 60).len();
|
||||||
|
}
|
||||||
|
std::io::stdout().flush().expect("Failed to flush stdout");
|
||||||
latest_msg_len = message.chars().count();
|
latest_msg_len = message.chars().count();
|
||||||
|
let mut second = 0;
|
||||||
for _ in 0..md.lenght / 2 {
|
for sample_index in 0..md.lenght {
|
||||||
|
if (sample_index / (md.sample_rate as u64 * 2)) > second {
|
||||||
|
second += 1;
|
||||||
|
if !args.no_backspace {
|
||||||
|
delete_chars(prev_timestamp_len);
|
||||||
|
let current_timestamp =
|
||||||
|
format!("({}:{:02} / {})", second / 60, second % 60, &total_lenght);
|
||||||
|
print!("{}", ¤t_timestamp);
|
||||||
|
std::io::stdout().flush().expect("Failed to flush stdout");
|
||||||
|
prev_timestamp_len = current_timestamp.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
if stream.read_exact(&mut buffer).is_err() {
|
if stream.read_exact(&mut buffer).is_err() {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let sample_l = byteorder::LittleEndian::read_i16(&buffer[..2]) as f32 / 32768.0;
|
|
||||||
let sample_r = byteorder::LittleEndian::read_i16(&buffer[2..]) as f32 / 32768.0;
|
samples[index] = byteorder::LittleEndian::read_i16(&buffer[..2]) as f32 / 32768.0;
|
||||||
// Left channel
|
|
||||||
samples[index] = match channel {
|
|
||||||
Channel::Left | Channel::Stereo => sample_l,
|
|
||||||
Channel::Right => {
|
|
||||||
if args.single {
|
|
||||||
0f32
|
|
||||||
} else {
|
|
||||||
sample_r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
index += 1;
|
|
||||||
// Right channel
|
|
||||||
samples[index] = match channel {
|
|
||||||
Channel::Right | Channel::Stereo => sample_r,
|
|
||||||
Channel::Left => {
|
|
||||||
if args.single {
|
|
||||||
0f32
|
|
||||||
} else {
|
|
||||||
sample_l
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
index += 1;
|
index += 1;
|
||||||
|
|
||||||
if index == BUFFER_SIZE {
|
if index == BUFFER_SIZE {
|
||||||
// Sink's thread is detached from main thread, so we need to synchronize with it
|
// Sink's thread is detached from main thread, so we need to synchronize with it
|
||||||
// Why we should synchronize with it?
|
// Why we should synchronize with it?
|
||||||
|
@ -123,20 +102,13 @@ fn main() {
|
||||||
while sink.len() >= CACHE_SIZE {
|
while sink.len() >= CACHE_SIZE {
|
||||||
// Sleeping exactly one buffer
|
// Sleeping exactly one buffer
|
||||||
std::thread::sleep(std::time::Duration::from_secs_f32(
|
std::thread::sleep(std::time::Duration::from_secs_f32(
|
||||||
(if sink.len() >= 2 {
|
BUFFER_SIZE as f32 / md.sample_rate as f32 / 2.0,
|
||||||
sink.len() - 2
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
} as f32) * BUFFER_SIZE as f32
|
|
||||||
/ 44100.0 / 2.0,
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
sink.append(SamplesBuffer::new(2, 44100, samples.as_slice()));
|
sink.append(SamplesBuffer::new(2, md.sample_rate, samples.as_slice()));
|
||||||
index = 0;
|
index = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while sink.len() != 0 {
|
sink.sleep_until_end()
|
||||||
std::thread::sleep(std::time::Duration::from_secs_f32(0.01))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,4 +20,4 @@ cargo lipo --release --targets aarch64-apple-ios-sim,x86_64-apple-ios
|
||||||
|
|
||||||
Open Xcode and run.
|
Open Xcode and run.
|
||||||
|
|
||||||
[Screenshots](./screenshots/swiftui)
|
[Screenshots (pre v0.2)](./screenshots/swiftui)
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
#ifndef MonoLib_Bridging_Header_h
|
#ifndef MonoLib_Bridging_Header_h
|
||||||
#define MonoLib_Bridging_Header_h
|
#define MonoLib_Bridging_Header_h
|
||||||
|
|
||||||
|
|
||||||
#import "monolib.h"
|
#import "monolib.h"
|
||||||
|
|
||||||
#endif /* MonoLib_Bridging_Header_h */
|
#endif /* MonoLib_Bridging_Header_h */
|
||||||
|
|
|
@ -25,14 +25,19 @@ class MonoLib {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
|
let timer = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()
|
||||||
@State private var server: String = ""
|
@State private var server: String = ""
|
||||||
@State private var port: String = ""
|
@State private var port: String = ""
|
||||||
@State private var playing: Bool = true
|
@State private var playing: Bool = true
|
||||||
@State private var running: Bool = false
|
@State private var running: Bool = false
|
||||||
|
|
||||||
|
@State var now_playing_artist: String = ""
|
||||||
|
@State var now_playing_album: String = ""
|
||||||
|
@State var now_playing_title: String = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
Text("Monoclient").font(.largeTitle).fontWidth(.expanded).bold() //.padding(.top, 25)
|
Text("Monoclient").font(.largeTitle).fontWidth(.expanded).bold()
|
||||||
VStack(alignment: .center) {
|
VStack(alignment: .center) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Server").frame(minWidth: 50, idealWidth: 60)
|
Text("Server").frame(minWidth: 50, idealWidth: 60)
|
||||||
|
@ -79,6 +84,15 @@ struct ContentView: View {
|
||||||
.bordered
|
.bordered
|
||||||
).disabled(!running)
|
).disabled(!running)
|
||||||
}.frame(width: 300)
|
}.frame(width: 300)
|
||||||
|
Text(now_playing_artist).font(.title2).onReceive(timer) { _ in
|
||||||
|
now_playing_artist = String(cString: get_metadata_artist()!)
|
||||||
|
}
|
||||||
|
Text(now_playing_album).onReceive(timer) { _ in
|
||||||
|
now_playing_album = String(cString: get_metadata_album()!)
|
||||||
|
}
|
||||||
|
Text(now_playing_title).font(.title).bold().onReceive(timer) { _ in
|
||||||
|
now_playing_title = String(cString: get_metadata_title()!)
|
||||||
|
}
|
||||||
|
|
||||||
}.padding()
|
}.padding()
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
[package]
|
[package]
|
||||||
name = "monolib"
|
name = "monolib"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rodio = { version = "0.17.3", default-features = false }
|
rodio = { version = "0.17.3", default-features = false }
|
||||||
byteorder = "1.5.0"
|
byteorder = "1.5.0"
|
||||||
|
rmp-serde = "1.1.2"
|
||||||
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "monolib"
|
name = "monolib"
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
use byteorder::ByteOrder;
|
use byteorder::ByteOrder;
|
||||||
use rodio::buffer::SamplesBuffer;
|
use rodio::buffer::SamplesBuffer;
|
||||||
use rodio::{OutputStream, Sink};
|
use rodio::{OutputStream, Sink};
|
||||||
use std::ffi::CStr;
|
use serde::Deserialize;
|
||||||
|
use std::ffi::{CStr, CString};
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
use std::os::raw::c_char;
|
use std::os::raw::c_char;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
// How many samples to cache before playing in samples (both channels) SHOULD BE EVEN
|
// How many samples to cache before playing in samples (both channels) SHOULD BE EVEN
|
||||||
const BUFFER_SIZE: usize = 2400;
|
const BUFFER_SIZE: usize = 2400;
|
||||||
|
@ -12,9 +14,16 @@ const BUFFER_SIZE: usize = 2400;
|
||||||
const CACHE_SIZE: usize = 40;
|
const CACHE_SIZE: usize = 40;
|
||||||
|
|
||||||
static mut SINK: Option<Box<Sink>> = None;
|
static mut SINK: Option<Box<Sink>> = None;
|
||||||
static mut RUNNING: bool = false;
|
static mut MD: Option<SentMetadata> = None;
|
||||||
static mut STOPPED: bool = false;
|
static mut STATE: State = State::NotStarted;
|
||||||
static mut RESET: bool = false;
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
enum State {
|
||||||
|
NotStarted,
|
||||||
|
Resetting,
|
||||||
|
Playing,
|
||||||
|
Paused,
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn start(server: *const c_char) {
|
pub extern "C" fn start(server: *const c_char) {
|
||||||
|
@ -30,13 +39,13 @@ pub extern "C" fn start(server: *const c_char) {
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn toggle() {
|
pub extern "C" fn toggle() {
|
||||||
unsafe {
|
unsafe {
|
||||||
if !STOPPED {
|
if STATE == State::Playing {
|
||||||
STOPPED = true;
|
STATE = State::Paused;
|
||||||
if let Some(sink) = &SINK {
|
if let Some(sink) = &SINK {
|
||||||
sink.pause();
|
sink.pause();
|
||||||
}
|
}
|
||||||
} else {
|
} else if STATE == State::Paused {
|
||||||
STOPPED = false;
|
STATE = State::Playing;
|
||||||
if let Some(sink) = &SINK {
|
if let Some(sink) = &SINK {
|
||||||
sink.play();
|
sink.play();
|
||||||
}
|
}
|
||||||
|
@ -47,19 +56,84 @@ pub extern "C" fn toggle() {
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn reset() {
|
pub extern "C" fn reset() {
|
||||||
unsafe {
|
unsafe {
|
||||||
RESET = true;
|
STATE = State::Resetting;
|
||||||
|
if let Some(sink) = &SINK {
|
||||||
|
sink.pause();
|
||||||
|
}
|
||||||
// Blocking main thread
|
// Blocking main thread
|
||||||
while RESET {
|
while STATE == State::Resetting {
|
||||||
std::thread::sleep(std::time::Duration::from_secs_f32(0.02))
|
std::thread::sleep(std::time::Duration::from_secs_f32(0.01))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn get_metadata_artist() -> *mut c_char {
|
||||||
|
unsafe {
|
||||||
|
match &MD {
|
||||||
|
Some(md) => CString::new(md.artist.clone()).unwrap().into_raw(),
|
||||||
|
_ => CString::new("").unwrap().into_raw(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn get_metadata_album() -> *mut c_char {
|
||||||
|
unsafe {
|
||||||
|
match &MD {
|
||||||
|
Some(md) => CString::new(md.album.clone()).unwrap().into_raw(),
|
||||||
|
_ => CString::new("").unwrap().into_raw(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn get_metadata_title() -> *mut c_char {
|
||||||
|
unsafe {
|
||||||
|
match &MD {
|
||||||
|
Some(md) => CString::new(md.title.clone()).unwrap().into_raw(),
|
||||||
|
_ => CString::new("").unwrap().into_raw(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn _reset() {
|
||||||
|
if let Some(sink) = &SINK {
|
||||||
|
sink.pause();
|
||||||
|
sink.clear();
|
||||||
|
}
|
||||||
|
SINK = None;
|
||||||
|
MD = None;
|
||||||
|
STATE = State::NotStarted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset - true, not - false
|
||||||
|
unsafe fn watching_sleep(dur: f32) -> bool {
|
||||||
|
let start = Instant::now();
|
||||||
|
while Instant::now() < start + std::time::Duration::from_secs_f32(dur) {
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs_f32(0.0001));
|
||||||
|
if STATE == State::Resetting {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
|
struct SentMetadata {
|
||||||
|
// In bytes, we need to read next track metadata
|
||||||
|
lenght: u64,
|
||||||
|
sample_rate: u32,
|
||||||
|
title: String,
|
||||||
|
album: String,
|
||||||
|
artist: String,
|
||||||
|
}
|
||||||
|
|
||||||
unsafe fn run(server: &str) {
|
unsafe fn run(server: &str) {
|
||||||
if RUNNING {
|
if STATE == State::Playing || STATE == State::Paused {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
RUNNING = true;
|
STATE = State::Playing;
|
||||||
let mut stream = TcpStream::connect(server).unwrap();
|
let mut stream = TcpStream::connect(server).unwrap();
|
||||||
println!("Connected to {} from {}", stream.peer_addr().unwrap(), stream.local_addr().unwrap());
|
println!("Connected to {} from {}", stream.peer_addr().unwrap(), stream.local_addr().unwrap());
|
||||||
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
|
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
|
||||||
|
@ -74,33 +148,41 @@ unsafe fn run(server: &str) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut buffer = [0u8; 4];
|
match &SINK {
|
||||||
|
None => {
|
||||||
|
let sink = Sink::try_new(&stream_handle).unwrap();
|
||||||
|
SINK = Some(Box::new(sink));
|
||||||
|
}
|
||||||
|
Some(s) => {
|
||||||
|
if s.is_paused() {
|
||||||
|
s.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut buffer = [0u8; 2];
|
||||||
let mut samples = [0f32; BUFFER_SIZE];
|
let mut samples = [0f32; BUFFER_SIZE];
|
||||||
|
loop {
|
||||||
let mut index = 0usize;
|
let mut index = 0usize;
|
||||||
while stream.read_exact(&mut buffer).is_ok() {
|
|
||||||
while STOPPED {
|
|
||||||
std::thread::sleep(std::time::Duration::from_secs_f32(0.5))
|
|
||||||
}
|
|
||||||
if RESET {
|
|
||||||
RUNNING = false;
|
|
||||||
STOPPED = false;
|
|
||||||
|
|
||||||
if let Some(sink) = &SINK {
|
let md: SentMetadata =
|
||||||
sink.pause();
|
rmp_serde::from_read(&stream).expect("Failed to parse track metadata");
|
||||||
sink.clear();
|
MD = Some(md.clone());
|
||||||
|
for _ in 0..md.lenght {
|
||||||
|
while STATE == State::Paused {
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs_f32(0.25))
|
||||||
}
|
}
|
||||||
SINK = None;
|
if STATE == State::Resetting {
|
||||||
RESET = false;
|
_reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let sample_l = byteorder::LittleEndian::read_i16(&buffer[..2]) as f32 / 32768.0;
|
|
||||||
let sample_r = byteorder::LittleEndian::read_i16(&buffer[2..]) as f32 / 32768.0;
|
if stream.read_exact(&mut buffer).is_err() {
|
||||||
// Left channel
|
return;
|
||||||
samples[index] = sample_l;
|
};
|
||||||
index += 1;
|
|
||||||
// Right channel
|
samples[index] = byteorder::LittleEndian::read_i16(&buffer[..2]) as f32 / 32768.0;
|
||||||
samples[index] = sample_r;
|
|
||||||
index += 1;
|
index += 1;
|
||||||
|
|
||||||
if index == BUFFER_SIZE {
|
if index == BUFFER_SIZE {
|
||||||
// Sink's thread is detached from main thread, so we need to synchronize with it
|
// Sink's thread is detached from main thread, so we need to synchronize with it
|
||||||
// Why we should synchronize with it?
|
// Why we should synchronize with it?
|
||||||
|
@ -108,19 +190,23 @@ unsafe fn run(server: &str) {
|
||||||
// a lot (no upper limit, actualy) of buffered sound, waiting for playing in sink
|
// a lot (no upper limit, actualy) of buffered sound, waiting for playing in sink
|
||||||
if let Some(sink) = &SINK {
|
if let Some(sink) = &SINK {
|
||||||
while sink.len() >= CACHE_SIZE {
|
while sink.len() >= CACHE_SIZE {
|
||||||
// Sleeping exactly one buffer
|
// Sleeping exactly one buffer and watching for reset signal
|
||||||
std::thread::sleep(std::time::Duration::from_secs_f32(
|
if watching_sleep(
|
||||||
(if sink.len() >= 2 {
|
if sink.len() > 2 {
|
||||||
sink.len() - 2
|
sink.len() as f32 - 2.0
|
||||||
} else {
|
} else {
|
||||||
1
|
0.5
|
||||||
} as f32) * BUFFER_SIZE as f32
|
} * BUFFER_SIZE as f32 / md.sample_rate as f32
|
||||||
/ 44100.0 / 2.0,
|
/ 2.0,
|
||||||
))
|
) {
|
||||||
|
_reset();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
sink.append(SamplesBuffer::new(2, 44100, samples.as_slice()));
|
}
|
||||||
|
sink.append(SamplesBuffer::new(2, md.sample_rate, samples.as_slice()));
|
||||||
index = 0;
|
index = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,3 +5,7 @@ void start(const char *server);
|
||||||
void toggle();
|
void toggle();
|
||||||
|
|
||||||
void reset();
|
void reset();
|
||||||
|
|
||||||
|
const char *get_metadata_artist();
|
||||||
|
const char *get_metadata_album();
|
||||||
|
const char *get_metadata_title();
|
||||||
|
|
53
src/main.rs
53
src/main.rs
|
@ -7,12 +7,10 @@ use clap::Parser;
|
||||||
use lofty::Accessor;
|
use lofty::Accessor;
|
||||||
use lofty::TaggedFileExt;
|
use lofty::TaggedFileExt;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use samplerate::ConverterType;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use symphonia::core::audio::SampleBuffer;
|
use symphonia::core::audio::SampleBuffer;
|
||||||
use symphonia::core::codecs::CODEC_TYPE_NULL;
|
use symphonia::core::codecs::CODEC_TYPE_NULL;
|
||||||
use symphonia::core::io::MediaSourceStream;
|
use symphonia::core::io::MediaSourceStream;
|
||||||
use symphonia::core::meta::StandardTagKey;
|
|
||||||
use symphonia::core::probe::Hint;
|
use symphonia::core::probe::Hint;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
|
@ -35,6 +33,8 @@ struct Args {
|
||||||
struct SentMetadata {
|
struct SentMetadata {
|
||||||
// In bytes, we need to read next track metadata
|
// In bytes, we need to read next track metadata
|
||||||
lenght: u64,
|
lenght: u64,
|
||||||
|
// Yep, no more interpolation
|
||||||
|
sample_rate: u32,
|
||||||
title: String,
|
title: String,
|
||||||
album: String,
|
album: String,
|
||||||
artist: String,
|
artist: String,
|
||||||
|
@ -42,23 +42,15 @@ struct SentMetadata {
|
||||||
|
|
||||||
async fn stream_samples(
|
async fn stream_samples(
|
||||||
track_samples: Vec<f32>,
|
track_samples: Vec<f32>,
|
||||||
rate: u32,
|
|
||||||
war: bool,
|
war: bool,
|
||||||
md: SentMetadata,
|
md: SentMetadata,
|
||||||
s: &mut TcpStream,
|
s: &mut TcpStream,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let resampled = if rate != 44100 {
|
|
||||||
samplerate::convert(rate, 44100, 2, ConverterType::Linear, track_samples.as_slice())
|
|
||||||
.unwrap()
|
|
||||||
} else {
|
|
||||||
track_samples
|
|
||||||
};
|
|
||||||
let mut md = md;
|
|
||||||
md.lenght = resampled.len() as u64;
|
|
||||||
if s.write_all(rmp_serde::to_vec(&md).unwrap().as_slice()).await.is_err() {
|
if s.write_all(rmp_serde::to_vec(&md).unwrap().as_slice()).await.is_err() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
for sample in resampled {
|
|
||||||
|
for sample in track_samples {
|
||||||
if s.write_all(
|
if s.write_all(
|
||||||
&(if war {
|
&(if war {
|
||||||
sample.signum() as i16 * 32767
|
sample.signum() as i16 * 32767
|
||||||
|
@ -121,7 +113,8 @@ async fn stream(mut s: TcpStream, tracklist: Arc<Vec<PathBuf>>) {
|
||||||
let mut file = std::fs::File::open(track).unwrap();
|
let mut file = std::fs::File::open(track).unwrap();
|
||||||
let tagged = lofty::read_from(&mut file).unwrap();
|
let tagged = lofty::read_from(&mut file).unwrap();
|
||||||
if let Some(id3v2) = tagged.primary_tag() {
|
if let Some(id3v2) = tagged.primary_tag() {
|
||||||
title = id3v2.title().unwrap_or("[No tag]".into()).to_string();
|
title =
|
||||||
|
id3v2.title().unwrap_or(track.file_stem().unwrap().to_string_lossy()).to_string();
|
||||||
album = id3v2.album().unwrap_or("[No tag]".into()).to_string();
|
album = id3v2.album().unwrap_or("[No tag]".into()).to_string();
|
||||||
artist = id3v2.artist().unwrap_or("[No tag]".into()).to_string()
|
artist = id3v2.artist().unwrap_or("[No tag]".into()).to_string()
|
||||||
};
|
};
|
||||||
|
@ -179,7 +172,7 @@ async fn stream(mut s: TcpStream, tracklist: Arc<Vec<PathBuf>>) {
|
||||||
.expect("unsupported codec");
|
.expect("unsupported codec");
|
||||||
|
|
||||||
let track_id = track.id;
|
let track_id = track.id;
|
||||||
let mut track_rate = 0;
|
let mut sample_rate = 0;
|
||||||
let mut samples = vec![];
|
let mut samples = vec![];
|
||||||
loop {
|
loop {
|
||||||
let packet = match format.next_packet() {
|
let packet = match format.next_packet() {
|
||||||
|
@ -188,18 +181,6 @@ async fn stream(mut s: TcpStream, tracklist: Arc<Vec<PathBuf>>) {
|
||||||
};
|
};
|
||||||
while !format.metadata().is_latest() {
|
while !format.metadata().is_latest() {
|
||||||
format.metadata().pop();
|
format.metadata().pop();
|
||||||
if let Some(rev) = format.metadata().current() {
|
|
||||||
for tag in rev.tags() {
|
|
||||||
println!("Looped");
|
|
||||||
match tag.std_key {
|
|
||||||
Some(StandardTagKey::Album) => album = tag.value.to_string(),
|
|
||||||
Some(StandardTagKey::Artist) => artist = tag.value.to_string(),
|
|
||||||
Some(StandardTagKey::TrackTitle) => title = tag.value.to_string(),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
eprintln!("DBG: {} {} {}", &album, &artist, &title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if packet.track_id() != track_id {
|
if packet.track_id() != track_id {
|
||||||
|
@ -208,7 +189,7 @@ async fn stream(mut s: TcpStream, tracklist: Arc<Vec<PathBuf>>) {
|
||||||
|
|
||||||
match decoder.decode(&packet) {
|
match decoder.decode(&packet) {
|
||||||
Ok(decoded) => {
|
Ok(decoded) => {
|
||||||
track_rate = decoded.spec().rate;
|
sample_rate = decoded.spec().rate;
|
||||||
let mut byte_buf =
|
let mut byte_buf =
|
||||||
SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec());
|
SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec());
|
||||||
byte_buf.copy_interleaved_ref(decoded);
|
byte_buf.copy_interleaved_ref(decoded);
|
||||||
|
@ -221,21 +202,15 @@ async fn stream(mut s: TcpStream, tracklist: Arc<Vec<PathBuf>>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !samples.is_empty() {
|
let md = SentMetadata {
|
||||||
if stream_samples(
|
lenght: samples.len() as u64,
|
||||||
samples,
|
sample_rate,
|
||||||
track_rate,
|
|
||||||
args.war,
|
|
||||||
SentMetadata {
|
|
||||||
lenght: 0,
|
|
||||||
title,
|
title,
|
||||||
album,
|
album,
|
||||||
artist,
|
artist,
|
||||||
},
|
};
|
||||||
&mut s,
|
if !samples.is_empty() {
|
||||||
)
|
if stream_samples(samples, args.war, md, &mut s).await {
|
||||||
.await
|
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue