From b043bd0a14b04ddc4cf69b4701c2e1e6fe8e0111 Mon Sep 17 00:00:00 2001 From: Kostya Shishkov Date: Wed, 7 Oct 2020 16:32:00 +0200 Subject: [PATCH] add nihav-sndplay --- sndplay/Cargo.toml | 12 + sndplay/README.md | 30 +++ sndplay/src/command.rs | 101 +++++++++ sndplay/src/main.rs | 500 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 643 insertions(+) create mode 100644 sndplay/Cargo.toml create mode 100644 sndplay/README.md create mode 100644 sndplay/src/command.rs create mode 100644 sndplay/src/main.rs 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 "] +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) { + 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, +} + +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, + dsupp: Box, + buf: &'a mut Vec, + 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, 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, 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) { + 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, Box)> = 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 = 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(); } +} -- 2.39.5