WIP sample rate

Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
This commit is contained in:
Ivan Bushchik 2024-03-13 18:37:55 +03:00
parent fd780cf28b
commit dd59f53d6f
No known key found for this signature in database
GPG key ID: 2F16FBF3262E090C
6 changed files with 299 additions and 113 deletions

View file

@ -7,9 +7,9 @@ use std::net::TcpStream;
use std::time::Instant;
// How many samples to cache before playing in samples (both channels) SHOULD BE EVEN
const BUFFER_SIZE: usize = 2400;
const BUFFER_SIZE: usize = 96000;
// How many buffers to cache
const CACHE_SIZE: usize = 100;
const CACHE_SIZE: usize = 20;
enum Channel {
Right,
@ -31,6 +31,14 @@ struct Args {
/// More verbose
#[arg(short)]
verbose: bool,
/// Stream in f32le instead of s16le
#[arg(short, long)]
float: bool,
/// Stream in custom sample rate
#[arg(short, long, default_value = "44100")]
sample_rate: u32,
}
fn main() {
@ -53,13 +61,28 @@ fn main() {
};
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
let sink = Sink::try_new(&stream_handle).unwrap();
let mut buffer = [0u8; 4];
let mut buffer = vec![
0u8;
if args.float {
8
} else {
4
}
];
let mut samples = [0f32; BUFFER_SIZE];
let mut index = 0usize;
let mut first = true;
while stream.read_exact(&mut buffer).is_ok() {
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;
let sample_l = if args.float {
byteorder::LittleEndian::read_f32(&buffer[..4])
} else {
byteorder::LittleEndian::read_i16(&buffer[..2]) as f32 / 32768.0
};
let sample_r = if args.float {
byteorder::LittleEndian::read_f32(&buffer[4..])
} else {
byteorder::LittleEndian::read_i16(&buffer[2..]) as f32 / 32768.0
};
// Left channel
samples[index] = match channel {
Channel::Left | Channel::Stereo => sample_l,
@ -102,14 +125,15 @@ fn main() {
} else {
1
} as f32) * BUFFER_SIZE as f32
/ 44100.0 / 2.0,
/ args.sample_rate as f32
/ 2.0,
))
}
if first && args.verbose {
eprintln!("Started playing in {} ms", (Instant::now() - start).as_millis());
first = false;
}
sink.append(SamplesBuffer::new(2, 44100, samples.as_slice()));
sink.append(SamplesBuffer::new(2, args.sample_rate, samples.as_slice()));
index = 0;
}
}

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="obG-Y5-kRd">
<rect key="frame" x="0.0" y="832" width="393" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="obG-Y5-kRd" secondAttribute="centerX" id="5cz-MP-9tL"/>
<constraint firstItem="obG-Y5-kRd" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" symbolic="YES" id="SfN-ll-jLj"/>
<constraint firstAttribute="bottom" secondItem="obG-Y5-kRd" secondAttribute="bottom" constant="20" id="Y44-ml-fuU"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

View file

@ -25,8 +25,8 @@ class MonoLib {
}
struct ContentView: View {
@State private var server: String = ""
@State private var port: String = ""
@State private var server: String = "ivabus.dev"
@State private var port: String = "5894"
@State private var playing: Bool = true
@State private var running: Bool = false
@ -56,14 +56,16 @@ struct ContentView: View {
Button(action: {
if running {
playing = !playing
toggle()
}
running = true
playing = !playing
} else {
let a = MonoLib()
Task.init {
await a.run(server: server + ":" + port)
}
running = true
playing = true
}
}) {
Image(
systemName: running
@ -73,8 +75,7 @@ struct ContentView: View {
.borderedProminent)
Button(action: {
reset()
running = false
playing = true
(running, playing) = (false, true)
}) { Image(systemName: "stop").font(.title3) }.buttonStyle(
.bordered
).disabled(!running)

View file

@ -5,16 +5,23 @@ use std::ffi::CStr;
use std::io::Read;
use std::net::TcpStream;
use std::os::raw::c_char;
use std::time::Instant;
// How many samples to cache before playing in samples (both channels) SHOULD BE EVEN
const BUFFER_SIZE: usize = 2400;
const BUFFER_SIZE: usize = 4410;
// How many buffers to cache
const CACHE_SIZE: usize = 40;
const CACHE_SIZE: usize = 20;
static mut SINK: Option<Box<Sink>> = None;
static mut RUNNING: bool = false;
static mut STOPPED: bool = false;
static mut RESET: bool = false;
static mut STATE: State = State::NotStarted;
#[derive(PartialEq)]
enum State {
NotStarted,
Resetting,
Playing,
Paused,
}
#[no_mangle]
pub extern "C" fn start(server: *const c_char) {
@ -30,13 +37,13 @@ pub extern "C" fn start(server: *const c_char) {
#[no_mangle]
pub extern "C" fn toggle() {
unsafe {
if !STOPPED {
STOPPED = true;
if STATE == State::Playing {
STATE = State::Paused;
if let Some(sink) = &SINK {
sink.pause();
}
} else {
STOPPED = false;
} else if STATE == State::Paused {
STATE = State::Playing;
if let Some(sink) = &SINK {
sink.play();
}
@ -47,19 +54,43 @@ pub extern "C" fn toggle() {
#[no_mangle]
pub extern "C" fn reset() {
unsafe {
RESET = true;
STATE = State::Resetting;
if let Some(sink) = &SINK {
sink.pause();
}
// Blocking main thread
while RESET {
std::thread::sleep(std::time::Duration::from_secs_f32(0.02))
while STATE == State::Resetting {
std::thread::sleep(std::time::Duration::from_secs_f32(0.01))
}
}
}
unsafe fn _reset() {
if let Some(sink) = &SINK {
sink.pause();
sink.clear();
}
SINK = 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
}
unsafe fn run(server: &str) {
if RUNNING {
if STATE == State::Playing || STATE == State::Paused {
return;
}
RUNNING = true;
STATE = State::Playing;
let mut stream = TcpStream::connect(server).unwrap();
println!("Connected to {} from {}", stream.peer_addr().unwrap(), stream.local_addr().unwrap());
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
@ -78,19 +109,11 @@ unsafe fn run(server: &str) {
let mut samples = [0f32; BUFFER_SIZE];
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))
while STATE == State::Paused {
std::thread::sleep(std::time::Duration::from_secs_f32(0.25))
}
if RESET {
RUNNING = false;
STOPPED = false;
if let Some(sink) = &SINK {
sink.pause();
sink.clear();
}
SINK = None;
RESET = false;
if STATE == State::Resetting {
_reset();
return;
}
let sample_l = byteorder::LittleEndian::read_i16(&buffer[..2]) as f32 / 32768.0;
@ -108,15 +131,18 @@ unsafe fn run(server: &str) {
// a lot (no upper limit, actualy) of buffered sound, waiting for playing in sink
if let Some(sink) = &SINK {
while sink.len() >= CACHE_SIZE {
// Sleeping exactly one buffer
std::thread::sleep(std::time::Duration::from_secs_f32(
(if sink.len() >= 2 {
sink.len() - 2
// Sleeping exactly one buffer and watching for reset signal
if watching_sleep(
if sink.len() > 2 {
sink.len() as f32 - 2.0
} else {
1
} as f32) * BUFFER_SIZE as f32
/ 44100.0 / 2.0,
))
0.5
} * BUFFER_SIZE as f32 / 44100.0
/ 2.0,
) {
_reset();
return;
}
}
sink.append(SamplesBuffer::new(2, 44100, samples.as_slice()));
index = 0;

View file

@ -1,10 +1,13 @@
use std::path::PathBuf;
use std::i16;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use chrono::Local;
use clap::Parser;
use rand::prelude::*;
use samplerate::ConverterType;
use rubato::{
Resampler, SincFixedIn, SincInterpolationParameters, SincInterpolationType, WindowFunction,
};
use symphonia::core::audio::SampleBuffer;
use symphonia::core::codecs::CODEC_TYPE_NULL;
use symphonia::core::io::MediaSourceStream;
@ -24,6 +27,14 @@ struct Args {
#[arg(short, long)]
war: bool,
/// Stream in f32le instead of s16le
#[arg(short, long)]
float: bool,
/// Stream in custom sample rate
#[arg(short, long, default_value = "44100")]
sample_rate: u32,
}
#[tokio::main]
@ -35,8 +46,7 @@ async fn main() {
.filter_entry(is_not_hidden)
.filter_map(|v| v.ok())
.map(|x| x.into_path())
.filter(track_valid)
.into_iter()
.filter(|arg0: &std::path::PathBuf| track_valid(arg0))
.collect::<Vec<PathBuf>>(),
);
loop {
@ -48,7 +58,7 @@ fn is_not_hidden(entry: &DirEntry) -> bool {
entry.file_name().to_str().map(|s| entry.depth() == 0 || !s.starts_with('.')).unwrap_or(false)
}
fn track_valid(track: &PathBuf) -> bool {
fn track_valid(track: &Path) -> bool {
if !track.metadata().unwrap().is_file() {
return false;
}
@ -60,36 +70,139 @@ fn track_valid(track: &PathBuf) -> bool {
true
}
async fn stream_samples(
args: &Args,
track_samples: Vec<f32>,
rate: u32,
s: &mut TcpStream,
) -> bool {
let params = SincInterpolationParameters {
sinc_len: 64,
f_cutoff: 0.96,
interpolation: SincInterpolationType::Quadratic,
oversampling_factor: 16,
window: WindowFunction::Blackman,
};
let target_rate = args.sample_rate;
let mut resampler = SincFixedIn::<f32>::new(
target_rate as f64 / rate as f64,
100.0,
params,
track_samples.len() / 2,
2,
)
.unwrap();
let start = std::time::Instant::now();
let (left, right): (Vec<&f32>, Vec<&f32>) =
(track_samples.iter().step_by(2).collect(), track_samples[1..].iter().step_by(2).collect());
eprintln!("Splitted channels in {} ms", (std::time::Instant::now() - start).as_millis());
let samples = if rate != target_rate {
eprintln!("Resampling {} samples from {} to {}", track_samples.len(), rate, target_rate);
let start = std::time::Instant::now();
if rate > target_rate {
let resampled_l =
samplerate::convert(rate, target_rate, 1, samplerate::ConverterType::Linear, &left)
.unwrap();
let resampled_r = samplerate::convert(
rate,
target_rate,
1,
samplerate::ConverterType::Linear,
right.as_slice(),
)
.unwrap();
eprintln!("Resampled in {} ms", (std::time::Instant::now() - start).as_millis());
vec![resampled_l, resampled_r]
} else {
match resampler.process(&[&left, &right], None) {
Ok(s) => {
eprintln!(
"Resampled in {} ms",
(std::time::Instant::now() - start).as_millis()
);
s
}
Err(e) => panic!("{}", e),
}
}
} else {
vec![left, right]
};
let (left, right) = (&samples[0], &samples[1]);
for (sample_l, sample_r) in left.iter().zip(right) {
if args.float {
let result = s
.write(
&(if args.war {
sample_l.signum()
} else {
*sample_l
})
.to_le_bytes(),
)
.await;
match result {
Err(_) | Ok(0) => return true,
_ => (),
};
let result = s
.write(
&(if args.war {
sample_r.signum()
} else {
*sample_r
})
.to_le_bytes(),
)
.await;
match result {
Err(_) | Ok(0) => return true,
_ => (),
};
} else {
let result = s
.write(
&(if args.war {
sample_l.signum() as i16 * 32767
} else {
(sample_l * 32768_f32) as i16
})
.to_le_bytes(),
)
.await;
match result {
Err(_) | Ok(0) => return true,
_ => (),
};
let result = s
.write(
&(if args.war {
sample_r.signum() as i16 * 32767
} else {
(sample_r * 32768_f32) as i16
})
.to_le_bytes(),
)
.await;
match result {
Err(_) | Ok(0) => return true,
_ => (),
};
}
}
eprintln!("Exiting");
false
}
async fn stream(mut s: TcpStream, tracklist: Arc<Vec<PathBuf>>) {
let args = Args::parse();
'track: loop {
loop {
s.writable().await.unwrap();
let track = tracklist.choose(&mut thread_rng()).unwrap();
eprintln!(
"[{}] {} to {}:{}{}",
Local::now().to_rfc3339(),
track.to_str().unwrap(),
s.peer_addr().unwrap().ip(),
s.peer_addr().unwrap().port(),
if args.war {
" with WAR.rs"
} else {
""
}
);
if args.public_log {
println!(
"[{}] {} to {}{}",
Local::now().to_rfc3339(),
track.to_str().unwrap(),
s.peer_addr().unwrap().port(),
if args.war {
" with WAR.rs"
} else {
""
}
);
println!("[{}] {}", Local::now().to_rfc3339(), track.to_str().unwrap(),);
}
let file = Box::new(std::fs::File::open(track).unwrap());
@ -118,11 +231,15 @@ async fn stream(mut s: TcpStream, tracklist: Arc<Vec<PathBuf>>) {
.expect("unsupported codec");
let track_id = track.id;
let mut track_samples = vec![];
let mut track_rate = 0;
loop {
let packet = match format.next_packet() {
Ok(packet) => packet,
_ => continue 'track,
_ => {
break;
}
};
while !format.metadata().is_latest() {
@ -136,46 +253,24 @@ async fn stream(mut s: TcpStream, tracklist: Arc<Vec<PathBuf>>) {
match decoder.decode(&packet) {
Ok(decoded) => {
let rate = decoded.spec().rate;
track_rate = rate;
let mut byte_buf =
SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec());
byte_buf.copy_interleaved_ref(decoded);
let samples = if rate != 44100 {
samplerate::convert(
rate,
44100,
2,
ConverterType::Linear,
byte_buf.samples(),
)
.unwrap()
} else {
byte_buf.samples().to_vec()
};
for sample in samples {
let result = s
.write(
&(if args.war {
sample.signum() as i16 * 32767
} else {
(sample * 32768_f32) as i16
})
.to_le_bytes(),
)
.await;
match result {
Err(_) | Ok(0) => {
return;
}
_ => (),
};
}
track_samples.append(&mut byte_buf.samples_mut().to_vec());
continue;
}
_ => {
// Handling any error as track skip
continue 'track;
continue;
}
}
}
if !track_samples.is_empty() {
if stream_samples(&args, track_samples, track_rate, &mut s).await {
return;
}
}
}
}