]> git.nihav.org Git - nihav-player.git/commitdiff
add nihav-sndplay
authorKostya Shishkov <kostya.shishkov@gmail.com>
Wed, 7 Oct 2020 14:32:00 +0000 (16:32 +0200)
committerKostya Shishkov <kostya.shishkov@gmail.com>
Wed, 7 Oct 2020 14:32:00 +0000 (16:32 +0200)
sndplay/Cargo.toml [new file with mode: 0644]
sndplay/README.md [new file with mode: 0644]
sndplay/src/command.rs [new file with mode: 0644]
sndplay/src/main.rs [new file with mode: 0644]

diff --git a/sndplay/Cargo.toml b/sndplay/Cargo.toml
new file mode 100644 (file)
index 0000000..3412441
--- /dev/null
@@ -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 (file)
index 0000000..e082775
--- /dev/null
@@ -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 (file)
index 0000000..802908b
--- /dev/null
@@ -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 (file)
index 0000000..66def2b
--- /dev/null
@@ -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(); }
+}