summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKostya Shishkov <kostya.shishkov@gmail.com>2020-10-07 16:32:00 +0200
committerKostya Shishkov <kostya.shishkov@gmail.com>2020-10-07 16:32:00 +0200
commitb043bd0a14b04ddc4cf69b4701c2e1e6fe8e0111 (patch)
tree240630cdcc3929e73bca945b13b4272ee618e264
parent113ac3d202140491de9a8ad1e75dd96882a8857f (diff)
downloadnihav-player-b043bd0a14b04ddc4cf69b4701c2e1e6fe8e0111.tar.gz
add nihav-sndplay
-rw-r--r--sndplay/Cargo.toml12
-rw-r--r--sndplay/README.md30
-rw-r--r--sndplay/src/command.rs101
-rw-r--r--sndplay/src/main.rs500
4 files changed, 643 insertions, 0 deletions
diff --git a/sndplay/Cargo.toml b/sndplay/Cargo.toml
new file mode 100644
index 0000000..3412441
--- /dev/null
+++ b/sndplay/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "nihav-sndplay"
+version = "0.1.0"
+authors = ["Kostya Shishkov <kostya.shishkov@gmail.com>"]
+edition = "2018"
+
+[dependencies]
+nihav_core = { path="../../nihav-core" }
+nihav_registry = { path="../../nihav-registry" }
+nihav_allstuff = { path="../../nihav-allstuff" }
+libc = "^0.2"
+sdl2-sys = "^0.34"
diff --git a/sndplay/README.md b/sndplay/README.md
new file mode 100644
index 0000000..e082775
--- /dev/null
+++ b/sndplay/README.md
@@ -0,0 +1,30 @@
+# nihav-sndplay
+
+nihav-sndplay is a minimalistic audio player based on NihAV and SDL2.
+
+## Getting Started
+
+In order to build it, put it into some subdirectory with other NihAV crates (or edit `Cargo.toml` to point to the proper crate locations) and invoke `cargo build`.
+
+Usage: `nihav-sndplay file1 file2 ...`.
+
+Recognized keyboard commands:
+* `escape` / `q` - quit
+* `space` - pause / resume playback
+* `enter` / `end` - play next file
+* `home` - play current track from the beginning
+* `left` / `right` - seek 10 seconds forward / back
+* `up` / `down` - seek 1 minute forward / back
+* `pgup` / `pgdn` - seek 10 minutes forward / back
+* `+` / `-` - increase / decrease volume
+* `m` - mute output
+
+## Contributing
+
+You're not supposed to. Even I hardly do that so why should you?
+
+## License
+
+NihAV is licensed under GNU Affero Public License - see [COPYING] for details.
+
+Parts of the project can be relicensed to other free licenses like LGPLv2 on request.
diff --git a/sndplay/src/command.rs b/sndplay/src/command.rs
new file mode 100644
index 0000000..802908b
--- /dev/null
+++ b/sndplay/src/command.rs
@@ -0,0 +1,101 @@
+use libc::{termios, tcgetattr, tcsetattr};
+use std::sync::mpsc;
+use std::io::Read;
+use std::thread;
+
+#[derive(Clone,Copy,Debug,PartialEq)]
+pub enum Command {
+ Debug,
+ Mute,
+ VolumeUp,
+ VolumeDown,
+ Pause,
+ Back(u8),
+ Forward(u8),
+ Repeat,
+ Next,
+ Quit,
+}
+
+pub struct CmdLineState {
+ orig_state: termios
+}
+impl CmdLineState {
+ pub fn new() -> Self {
+ let mut orig_state: termios = unsafe { std::mem::uninitialized() };
+ unsafe { tcgetattr(0, &mut orig_state); }
+ let mut new_state = orig_state;
+ new_state.c_lflag &= !(libc::ECHO | libc::ICANON);
+ unsafe { tcsetattr(0, 0, &new_state); }
+ Self { orig_state }
+ }
+ pub fn restore(&self) {
+ unsafe { tcsetattr(0, 0, &self.orig_state); }
+ }
+}
+
+pub fn start_reader() -> (thread::JoinHandle<()>, mpsc::Receiver<Command>) {
+ let (sender, cmd_receiver) = mpsc::sync_channel(100);
+ (thread::spawn(move || {
+ let stdin = std::io::stdin();
+ let mut file = stdin.lock();
+ let mut ch = [0u8; 8];
+ loop {
+/*
+\e char -> alt-char (including alt-[)
+\e [ char -> A - up B - down C - right D - left F - end H - home (or \e[OF/OH)
+\e [ O P-S -> F1-F4
+\e [ num ~ -> 5 - PgUp, 6 - PgDn, 15, 17-24 - F5,F6-F12
+*/
+ match file.read(&mut ch) {
+ Ok(1) => {
+ match ch[0] {
+ b'\n' => { sender.send(Command::Next).unwrap(); },
+ b' ' => { sender.send(Command::Pause).unwrap(); },
+ b'q' | b'Q' | 0o33 => {
+ sender.send(Command::Quit).unwrap();
+ break;
+ },
+ b'+' => { sender.send(Command::VolumeUp).unwrap(); },
+ b'-' => { sender.send(Command::VolumeDown).unwrap(); },
+ b'd' | b'D' => { sender.send(Command::Debug).unwrap(); },
+ b'm' | b'M' => { sender.send(Command::Mute).unwrap(); },
+ _ => {},
+ };
+ },
+ Ok(3) => {
+ if ch[0] == 0o33 {
+ if ch[1] == b'[' {
+ match ch[2] {
+ b'D' => { sender.send(Command::Back(1)).unwrap(); },
+ b'B' => { sender.send(Command::Back(2)).unwrap(); },
+ b'C' => { sender.send(Command::Forward(1)).unwrap(); },
+ b'A' => { sender.send(Command::Forward(2)).unwrap(); },
+ b'F' => { sender.send(Command::Repeat).unwrap(); },
+ b'H' => { sender.send(Command::Next).unwrap(); },
+ _ => {},
+ };
+ } else if ch[1] == b'O' {
+ match ch[2] {
+ b'F' => { sender.send(Command::Repeat).unwrap(); },
+ b'H' => { sender.send(Command::Next).unwrap(); },
+ _ => {},
+ };
+ }
+ }
+ },
+ Ok(4) => {
+ if ch[0] == 0o33 && ch[1] == b'[' && ch[3] == b'~' {
+ match ch[2] {
+ b'5' => { sender.send(Command::Forward(3)).unwrap(); },
+ b'6' => { sender.send(Command::Back(3)).unwrap(); },
+ _ => {},
+ };
+ }
+ },
+ Ok(_) => {},
+ Err(_) => break,
+ }
+ }
+ }), cmd_receiver)
+} \ No newline at end of file
diff --git a/sndplay/src/main.rs b/sndplay/src/main.rs
new file mode 100644
index 0000000..66def2b
--- /dev/null
+++ b/sndplay/src/main.rs
@@ -0,0 +1,500 @@
+extern crate libc;
+extern crate sdl2_sys;
+extern crate nihav_core;
+extern crate nihav_registry;
+extern crate nihav_allstuff;
+
+use std::fs::File;
+use std::io::prelude::*;
+use std::io::{BufReader, SeekFrom};
+use std::sync::mpsc;
+use std::time::Duration;
+use std::thread;
+use sdl2_sys::{SDL_AudioSpec, Uint32};
+use nihav_core::io::byteio::{FileReader, ByteReader};
+use nihav_core::frame::*;
+use nihav_core::codecs::*;
+use nihav_core::demuxers::*;
+use nihav_core::soundcvt::*;
+use nihav_registry::detect;
+use nihav_allstuff::*;
+
+mod command;
+use command::*;
+
+struct Player {
+ ended: bool,
+ dmx_reg: RegisteredDemuxers,
+ dec_reg: RegisteredDecoders,
+ paused: bool,
+ mute: bool,
+ volume: u8,
+ debug: bool,
+ buf: Vec<i16>,
+}
+
+struct AudioDevice {
+ device_id: sdl2_sys::SDL_AudioDeviceID
+}
+
+impl AudioDevice {
+ fn open(arate: u32, channels: u8) -> Option<(Self, SDL_AudioSpec)> {
+ let desired_spec = SDL_AudioSpec {
+ freq: arate as i32,
+ format: sdl2_sys::AUDIO_S16 as u16,
+ channels,
+ silence: 0,
+ samples: 0,
+ padding: 0,
+ size: 0,
+ callback: None,
+ userdata: std::ptr::null_mut(),
+ };
+ let mut dspec = desired_spec;
+ let device_id = unsafe { sdl2_sys::SDL_OpenAudioDevice(std::ptr::null(), 0, &desired_spec, &mut dspec, 0) };
+ if device_id != 0 {
+ Some((AudioDevice { device_id }, dspec))
+ } else {
+ None
+ }
+ }
+ fn pause(&self) {
+ unsafe { sdl2_sys::SDL_PauseAudioDevice(self.device_id, 1); }
+ }
+ fn resume(&self) {
+ unsafe { sdl2_sys::SDL_PauseAudioDevice(self.device_id, 0); }
+ }
+ fn clear(&self) {
+ unsafe { sdl2_sys::SDL_ClearQueuedAudio(self.device_id); }
+ }
+ fn queue(&self, buf: &[i16]) {
+ unsafe {
+ let len = buf.len() * 2;
+ let buf_ptr = buf.as_ptr();
+ sdl2_sys::SDL_QueueAudio(self.device_id, buf_ptr as *const core::ffi::c_void, len as Uint32);
+ }
+ }
+ fn queue_bytes(&self, buf: &[u8]) {
+ unsafe {
+ let len = buf.len();
+ let buf_ptr = buf.as_ptr();
+ sdl2_sys::SDL_QueueAudio(self.device_id, buf_ptr as *const core::ffi::c_void, len as Uint32);
+ }
+ }
+ fn size(&self) -> u32 {
+ unsafe { sdl2_sys::SDL_GetQueuedAudioSize(self.device_id) }
+ }
+}
+
+struct Decoder<'a> {
+ demuxer: Demuxer<'a>,
+ decoder: Box<dyn NADecoder>,
+ dsupp: Box<NADecoderSupport>,
+ buf: &'a mut Vec<i16>,
+ stream_no: usize,
+ dst_info: NAAudioInfo,
+ dst_chmap: NAChannelMap,
+ samplepos: u64,
+ arate: u32,
+ volume: u8,
+ mute: bool,
+}
+
+fn output_vol_i16(device: &AudioDevice, tmp: &mut Vec<i16>, src: &[i16], mute: bool, volume: u8) {
+ if !mute {
+ tmp.truncate(0);
+ tmp.reserve(src.len());
+ let vol = i32::from(volume);
+ for &sample in src.iter() {
+ let nsamp = vol * i32::from(sample) / 100;
+ tmp.push(nsamp.min(32767).max(-32768) as i16);
+ }
+ } else {
+ tmp.truncate(0);
+ tmp.resize(src.len(), 0);
+ }
+ device.queue(&tmp);
+}
+
+fn output_vol_u8(device: &AudioDevice, tmp: &mut Vec<i16>, src: &[u8], mute: bool, volume: u8) {
+ if !mute {
+ tmp.truncate(0);
+ tmp.reserve(src.len());
+ let vol = i32::from(volume);
+ for sample in src.chunks_exact(2) {
+ let sample = (u16::from(sample[0]) + u16::from(sample[1]) * 256) as i16;
+ let nsamp = vol * i32::from(sample) / 100;
+ tmp.push(nsamp.min(32767).max(-32768) as i16);
+ }
+ } else {
+ tmp.truncate(0);
+ tmp.resize(src.len() / 2, 0);
+ }
+ device.queue(&tmp);
+}
+
+impl<'a> Decoder<'a> {
+ fn refill(&mut self, device: &AudioDevice) -> bool {
+ loop {
+ match self.demuxer.get_frame() {
+ Ok(pkt) => {
+ if pkt.get_stream().get_num() == self.stream_no {
+ match self.decoder.decode(&mut self.dsupp, &pkt) {
+ Ok(frm) => {
+ let buf = frm.get_buffer();
+ if let Some(pts) = frm.ts.get_pts() {
+ self.samplepos = NATimeInfo::ts_to_time(pts, u64::from(self.arate), frm.ts.tb_num, frm.ts.tb_den);
+ }
+ let out_buf = convert_audio_frame(&buf, &self.dst_info, &self.dst_chmap).unwrap();
+ match out_buf {
+ NABufferType::AudioI16(abuf) => {
+ if !self.mute && self.volume == 100 {
+ device.queue(&abuf.get_data());
+ } else {
+ output_vol_i16(device, self.buf, &abuf.get_data(), self.mute, self.volume);
+ }
+ self.samplepos += abuf.get_length() as u64;
+ },
+ NABufferType::AudioPacked(abuf) => {
+ if !self.mute && self.volume == 100 {
+ device.queue_bytes(&abuf.get_data());
+ } else {
+ output_vol_u8(device, self.buf, &abuf.get_data(), self.mute, self.volume);
+ }
+ self.samplepos += abuf.get_length() as u64;
+ },
+ _ => println!("unknown buffer type"),
+ };
+ return false;
+ },
+ Err(err) => {
+ println!(" error decoding {:?}", err);
+ return true;
+ },
+ };
+ }
+ },
+ Err(DemuxerError::EOF) => return true,
+ Err(err) => {
+ println!("demuxing error {:?}", err);
+ return true;
+ },
+ };
+ }
+ }
+ fn seek(&mut self, time: u64) -> bool {
+ let ret = self.demuxer.seek(NATimePoint::Milliseconds(time));
+if ret.is_err() { println!(" seek error\n"); }
+ ret.is_ok()
+ }
+}
+
+fn format_time(ms: u64) -> String {
+ let s = ms / 1000;
+ let ds = (ms % 1000) / 100;
+ let (min, s) = (s / 60, s % 60);
+ if min == 0 {
+ format!("{}.{}", s, ds)
+ } else {
+ format!("{}:{:02}.{}", min, s, ds)
+ }
+}
+
+impl Player {
+ fn new() -> Self {
+ let mut dmx_reg = RegisteredDemuxers::new();
+ nihav_register_all_demuxers(&mut dmx_reg);
+ let mut dec_reg = RegisteredDecoders::new();
+ nihav_register_all_decoders(&mut dec_reg);
+
+ unsafe {
+ if sdl2_sys::SDL_Init(sdl2_sys::SDL_INIT_AUDIO) != 0 {
+ panic!("cannot init SDL");
+ }
+ }
+
+ Self {
+ ended: false,
+ paused: false,
+ dmx_reg, dec_reg,
+ volume: 100,
+ mute: false,
+ debug: false,
+ buf: Vec::new(),
+ }
+ }
+ fn play_file(&mut self, name: &str, cmd_receiver: &mpsc::Receiver<Command>) {
+ let ret = File::open(name);
+ if ret.is_err() {
+ println!("error opening {}", name);
+ return;
+ }
+ let mut file = ret.unwrap();
+
+ let mut fr = FileReader::new_read(&mut file);
+ let mut br = ByteReader::new(&mut fr);
+ let res = detect::detect_format(name, &mut br);
+ if res.is_none() {
+ println!("cannot detect format for {}", name);
+ return;
+ }
+ let (dmx_name, _) = res.unwrap();
+ drop(br);
+ drop(fr);
+ let dmx_fact = self.dmx_reg.find_demuxer(dmx_name);
+ if dmx_fact.is_none() {
+ println!("no demuxer for format {}", dmx_name);
+ return;
+ }
+ let dmx_fact = dmx_fact.unwrap();
+
+ file.seek(SeekFrom::Start(0)).unwrap();
+ let mut file = BufReader::new(file);
+ let mut fr = FileReader::new_read(&mut file);
+ let mut br = ByteReader::new(&mut fr);
+ let res = create_demuxer(dmx_fact, &mut br);
+ if res.is_err() {
+ println!("cannot create demuxer");
+ return;
+ }
+ let dmx = res.unwrap();
+
+ let mut ainfo = None;
+ let mut dec: Option<(Box<NADecoderSupport>, Box<dyn NADecoder>)> = None;
+ let mut stream_no = 0;
+ let mut duration = dmx.get_duration();
+ for i in 0..dmx.get_num_streams() {
+ let s = dmx.get_stream(i).unwrap();
+ let info = s.get_info();
+ if info.is_audio() {
+ let decfunc = self.dec_reg.find_decoder(info.get_name());
+ if decfunc.is_none() {
+ println!("no decoder for {}", info.get_name());
+ continue;
+ }
+ let mut decoder = (decfunc.unwrap())();
+ let mut dsupp = Box::new(NADecoderSupport::new());
+ if decoder.init(&mut dsupp, info.clone()).is_err() {
+ println!("cannot init decoder for stream {}", i);
+ continue;
+ }
+ dec = Some((dsupp, decoder));
+ ainfo = Some(info);
+ stream_no = i;
+ if s.duration > 0 {
+ duration = NATimeInfo::ts_to_time(s.duration, 1000, s.tb_num, s.tb_den);
+ }
+ break;
+ }
+ }
+ if dec.is_none() {
+ println!("no audio decoder found");
+ return;
+ }
+ let (dsupp, decoder) = dec.unwrap();
+
+ let ainfo = ainfo.unwrap().get_properties().get_audio_info().unwrap();
+ let arate = if ainfo.sample_rate > 0 { ainfo.sample_rate } else { 44100 };
+ let ch = ainfo.channels;
+
+ println!("Playing {} [{}Hz {}ch]", name, arate, ch);
+ let ret = AudioDevice::open(arate, ch.max(2));
+ if ret.is_none() {
+ println!("cannot open output");
+ return;
+ }
+ let (device, dspec) = ret.unwrap();
+ let block_limit = dmx.get_stream(stream_no).unwrap().tb_num * arate / dmx.get_stream(stream_no).unwrap().tb_den * u32::from(dspec.channels);
+
+ let dst_info = NAAudioInfo { sample_rate: dspec.freq as u32, channels: dspec.channels, format: SND_S16_FORMAT, block_len: 1 };
+ let dst_chmap = if dst_info.channels == 2 {
+ NAChannelMap::from_str("L,R").unwrap()
+ } else {
+ NAChannelMap::from_str("C").unwrap()
+ };
+ let mut decoder = Decoder {
+ demuxer: dmx,
+ decoder, dsupp,
+ stream_no,
+ dst_info, dst_chmap,
+ samplepos: 0,
+ arate,
+ volume: self.volume,
+ mute: self.mute,
+ buf: &mut self.buf,
+ };
+ let mut refill_limit = arate * u32::from(dspec.channels);
+ let underfill_limit = (arate * u32::from(dspec.channels) / 4).max(block_limit);
+
+ let mut eof = decoder.refill(&device);
+ while !eof && device.size() < refill_limit {
+ eof = decoder.refill(&device);
+ }
+
+ let duration_str = if duration != 0 { format_time(duration) } else { "???".to_owned() };
+ if !self.paused {
+ device.resume();
+ }
+ 'main: loop {
+ let cur_time = decoder.samplepos.saturating_sub(u64::from(device.size() / 2 / u32::from(dst_info.channels)));
+ let full_ms = cur_time * 1000 / u64::from(arate);
+ let timestr = format_time(full_ms);
+ let disp_vol = if self.mute { 0 } else { self.volume };
+ if !self.debug {
+ print!("> {} / {} {}% \r", timestr, duration_str, disp_vol);
+ } else {
+ print!("> {} / {} |{}| {}% \r", timestr, duration_str, device.size(), disp_vol);
+ }
+ std::io::stdout().flush().unwrap();
+ if device.size() < underfill_limit && !self.paused && refill_limit < (1 << 20) {
+ if full_ms > 5000 {
+ println!("underrun!");
+ }
+ refill_limit += refill_limit >> 1;
+ }
+ if device.size() < refill_limit / 2 {
+ while !eof && device.size() < refill_limit {
+ eof = decoder.refill(&device);
+ }
+ }
+ if eof && device.size() == 0 {
+ break 'main;
+ }
+ while let Ok(cmd) = cmd_receiver.try_recv() {
+ let cur_time = decoder.samplepos.saturating_sub(u64::from(device.size() / 2 / u32::from(dst_info.channels)));
+ match cmd {
+ Command::Forward(val) => {
+ device.pause();
+ device.clear();
+ let seekoff = match val {
+ 1 => 10,
+ 2 => 60,
+ _ => 10 * 60,
+ };
+ let seek_time = cur_time * 1000 / u64::from(arate) + seekoff * 1000;
+ let _ret = decoder.seek(seek_time);
+ while !eof && device.size() < refill_limit {
+ eof = decoder.refill(&device);
+ }
+ if eof {
+ break 'main;
+ }
+ if !self.paused {
+ device.resume();
+ }
+ },
+ Command::Back(val) => {
+ device.pause();
+ device.clear();
+ let seekoff = match val {
+ 1 => 10,
+ 2 => 60,
+ _ => 10 * 60,
+ };
+ let seek_time = (cur_time * 1000 / u64::from(arate)).saturating_sub(seekoff * 1000);
+ let _ret = decoder.seek(seek_time);
+ while !eof && device.size() < refill_limit {
+ eof = decoder.refill(&device);
+ }
+ if eof {
+ break 'main;
+ }
+ if !self.paused {
+ device.resume();
+ }
+ },
+ Command::Quit => {
+ device.pause();
+ self.ended = true;
+ break 'main;
+ },
+ Command::Next => {
+ device.pause();
+ break 'main;
+ },
+ Command::Repeat => {
+ device.pause();
+ device.clear();
+ let _ret = decoder.seek(0);
+ while !eof && device.size() < refill_limit {
+ eof = decoder.refill(&device);
+ }
+ if eof {
+ break 'main;
+ }
+ if !self.paused {
+ device.resume();
+ }
+ },
+ Command::Pause => {
+ self.paused = !self.paused;
+ if self.paused {
+ device.pause();
+ } else {
+ device.resume();
+ }
+ },
+ Command::VolumeUp => {
+ self.volume = (self.volume + 10).min(200);
+ decoder.volume = self.volume;
+ },
+ Command::VolumeDown => {
+ self.volume = self.volume.saturating_sub(10);
+ decoder.volume = self.volume;
+ },
+ Command::Mute => {
+ self.mute = !self.mute;
+ decoder.mute = self.mute;
+ },
+ Command::Debug => {
+ self.debug = !self.debug;
+ },
+ };
+ print!("\r{:60}\r", ' ');
+ }
+ thread::sleep(Duration::from_millis(200));
+ }
+
+ println!();
+ }
+}
+
+fn main() {
+ let args: Vec<String> = std::env::args().collect();
+
+ let cmd_state = CmdLineState::new();
+
+ let (cmd_reader_thread, cmd_receiver) = start_reader();
+ let mut player = Player::new();
+
+ if args.len() == 1 {
+ println!("usage: nihav-sndplay file1 file2 ...");
+ return;
+ }
+
+ if args[1] == "--help" {
+ println!("usage: nihav-sndplay file1 file2 ...");
+ println!("commands:");
+ println!(" escape / q - quit");
+ println!(" space - pause / resume playback");
+ println!(" enter / end - play next file");
+ println!(" home - play current track from the beginning");
+ println!(" left / right - seek 10 seconds forward / back");
+ println!(" up / down - seek 1 minute forward / back");
+ println!(" pgup / pgdn - seek 10 minutes forward / back");
+ println!(" + / - - increase / decrease volume");
+ println!(" m - mute output");
+ return;
+ }
+
+ for arg in args[1..].iter() {
+ player.play_file(arg, &cmd_receiver);
+ if player.ended {
+ break;
+ }
+ }
+ cmd_state.restore();
+
+ drop(cmd_reader_thread);
+ unsafe { sdl2_sys::SDL_Quit(); }
+}