Add some built-in and code documentation

This commit is contained in:
NGnius (Graham) 2022-01-03 21:15:28 -05:00
parent 5ef1b4a2b8
commit e6e52ddb58
20 changed files with 329 additions and 198 deletions

6
Cargo.lock generated
View file

@ -634,7 +634,7 @@ dependencies = [
[[package]]
name = "mps"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"clap",
"mps-interpreter",
@ -643,7 +643,7 @@ dependencies = [
[[package]]
name = "mps-interpreter"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"dirs",
"regex",
@ -654,7 +654,7 @@ dependencies = [
[[package]]
name = "mps-player"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"m3u8-rs",
"mpris-player",

View file

@ -1,6 +1,6 @@
[package]
name = "mps"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
authors = ["NGnius (Graham) <ngniusness@gmail.com>"]
description = "Music Playlist Scripting language (MPS)"
@ -15,7 +15,7 @@ members = [
[dependencies]
# local
mps-interpreter = { version = "0.1.0", path = "./mps-interpreter" }
mps-player = { version = "0.1.0", path = "./mps-player" }
mps-interpreter = { version = "0.2.0", path = "./mps-interpreter" }
mps-player = { version = "0.2.0", path = "./mps-player" }
# external
clap = { version = "3.0", features = ["derive"] }

View file

@ -1,9 +1,9 @@
# mps
An MPS program which plays music.
This doesn't do much yet, since mps-interpreter is still under construction.
Future home of a MPS REPL for playing music ergonomically through a CLI.
This project implements the interpreter (mps-interpreter), music player (mps-player), and CLI interface for MPS (root).
The CLI interface includes a REPL for running scripts.
The REPL interactive mode also provides more details about using MPS through the `?help` command.
License: LGPL-2.1-only OR GPL-2.0-or-later

View file

@ -1,6 +1,6 @@
[package]
name = "mps-interpreter"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
license = "LGPL-2.1-only OR GPL-2.0-or-later"
readme = "README.md"

View file

@ -8,6 +8,8 @@ use super::tokens::MpsToken;
use super::MpsContext;
use super::MpsMusicItem;
/// The script interpreter.
/// Use MpsRunner for a better interface.
pub struct MpsInterpretor<T>
where
T: crate::tokens::MpsTokenReader,
@ -151,6 +153,7 @@ fn box_error_with_ctx<E: MpsLanguageError + 'static>(
Box::new(error) as Box<dyn MpsLanguageError>
}
/// Builder function to add the standard statements of MPS.
pub(crate) fn standard_vocab(vocabulary: &mut MpsLanguageDictionary) {
vocabulary
.add(crate::lang::vocabulary::filters::empty_filter())

View file

@ -2,7 +2,7 @@ use std::collections::VecDeque;
//use std::fmt::{Debug, Display, Error, Formatter};
use std::marker::PhantomData;
use crate::lang::utility::{assert_token, assert_token_raw, assert_token_raw_back, assert_empty};
use crate::lang::utility::{assert_empty, assert_token, assert_token_raw, assert_token_raw_back};
use crate::lang::MpsLanguageDictionary;
use crate::lang::SyntaxError;
use crate::lang::{BoxedMpsOpFactory, MpsOp};

View file

@ -23,6 +23,7 @@ impl<T: MpsTokenReader> MpsRunnerSettings<T> {
}
}
/// A wrapper around MpsInterpretor which provides a simpler (and more powerful) interface.
pub struct MpsRunner<T: MpsTokenReader> {
interpretor: MpsInterpretor<T>,
new_statement: bool,

View file

@ -1,6 +1,6 @@
[package]
name = "mps-player"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
license = "LGPL-2.1-only OR GPL-2.0-or-later"
readme = "README.md"
@ -10,7 +10,7 @@ rodio = { version = "^0.14"}
m3u8-rs = { version = "^3.0.0" }
# local
mps-interpreter = { path = "../mps-interpreter" }
mps-interpreter = { path = "../mps-interpreter", version = "0.2.0" }
[target.'cfg(unix)'.dependencies]
#dbus = { version = "^0.9" }

View file

@ -1,6 +1,8 @@
# mps-player
An MPS playback library with support for Linux media controls (D-Bus).
An MPS playback library with support for media controls (Linux & D-Bus only atm).
This handles the output from interpreting a script.
Music playback and m3u8 playlist generation are implemented in this part of the project.
License: LGPL-2.1-only OR GPL-2.0-or-later

View file

@ -1,18 +1,20 @@
use std::sync::mpsc::{Sender, Receiver, channel};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::thread::JoinHandle;
use mps_interpreter::tokens::MpsTokenReader;
use super::os_controls::SystemControlWrapper;
use super::player_wrapper::{ControlAction, MpsPlayerServer, PlayerAction};
use super::MpsPlayer;
use super::PlaybackError;
use super::player_wrapper::{ControlAction, PlayerAction, MpsPlayerServer};
use super::os_controls::SystemControlWrapper;
/// A controller for a MpsPlayer running on another thread.
/// This receives and sends events like media buttons and script errors for the MpsPlayer.
pub struct MpsController {
control: Sender<ControlAction>,
event: Receiver<PlayerAction>,
handle: JoinHandle<()>,
sys_ctrl: SystemControlWrapper
sys_ctrl: SystemControlWrapper,
}
impl MpsController {
@ -23,7 +25,8 @@ impl MpsController {
let (event_tx, event_rx) = channel();
let mut sys_ctrl = SystemControlWrapper::new(control_tx.clone());
sys_ctrl.init();
let handle = MpsPlayerServer::spawn(player_gen, control_tx.clone(), control_rx, event_tx, false);
let handle =
MpsPlayerServer::spawn(player_gen, control_tx.clone(), control_rx, event_tx, false);
Self {
control: control_tx,
event: event_rx,
@ -39,7 +42,8 @@ impl MpsController {
let (event_tx, event_rx) = channel();
let mut sys_ctrl = SystemControlWrapper::new(control_tx.clone());
sys_ctrl.init();
let handle = MpsPlayerServer::spawn(player_gen, control_tx.clone(), control_rx, event_tx, true);
let handle =
MpsPlayerServer::spawn(player_gen, control_tx.clone(), control_rx, event_tx, true);
Self {
control: control_tx,
event: event_rx,
@ -49,7 +53,9 @@ impl MpsController {
}
fn send_confirm(&self, to_send: ControlAction) -> Result<(), PlaybackError> {
self.control.send(to_send.clone()).map_err(PlaybackError::from_err)?;
self.control
.send(to_send.clone())
.map_err(PlaybackError::from_err)?;
let mut response = self.event.recv().map_err(PlaybackError::from_err)?;
while !response.is_acknowledgement() {
Self::handle_event(response)?;
@ -60,12 +66,13 @@ impl MpsController {
Ok(())
} else {
Err(PlaybackError {
msg: "Incorrect acknowledgement received for MpsController control action".into()
msg: "Incorrect acknowledgement received for MpsController control action"
.into(),
})
}
} else {
Err(PlaybackError {
msg: "Invalid acknowledgement received for MpsController control action".into()
msg: "Invalid acknowledgement received for MpsController control action".into(),
})
}
}
@ -80,41 +87,44 @@ impl MpsController {
}
pub fn next(&self) -> Result<(), PlaybackError> {
self.send_confirm(ControlAction::Next{ack: true})
self.send_confirm(ControlAction::Next { ack: true })
}
pub fn previous(&self) -> Result<(), PlaybackError> {
self.send_confirm(ControlAction::Previous{ack: true})
self.send_confirm(ControlAction::Previous { ack: true })
}
pub fn play(&self) -> Result<(), PlaybackError> {
self.send_confirm(ControlAction::Play{ack: true})
self.send_confirm(ControlAction::Play { ack: true })
}
pub fn pause(&self) -> Result<(), PlaybackError> {
self.send_confirm(ControlAction::Pause{ack: true})
self.send_confirm(ControlAction::Pause { ack: true })
}
pub fn stop(&self) -> Result<(), PlaybackError> {
self.send_confirm(ControlAction::Stop{ack: true})
self.send_confirm(ControlAction::Stop { ack: true })
}
pub fn enqueue(&self, count: usize) -> Result<(), PlaybackError> {
self.send_confirm(ControlAction::Enqueue{amount: count, ack: true})
self.send_confirm(ControlAction::Enqueue {
amount: count,
ack: true,
})
}
pub fn ping(&self) -> Result<(), PlaybackError> {
self.send_confirm(ControlAction::NoOp{ack: true})
self.send_confirm(ControlAction::NoOp { ack: true })
}
pub fn exit(self) -> Result<(), PlaybackError> {
self.send_confirm(ControlAction::Exit{ack: true})?;
self.send_confirm(ControlAction::Exit { ack: true })?;
self.sys_ctrl.exit();
match self.handle.join() {
Ok(x) => Ok(x),
Err(_) => Err(PlaybackError {
msg: "MpsPlayerServer did not exit correctly".into()
})
msg: "MpsPlayerServer did not exit correctly".into(),
}),
}
}
@ -134,7 +144,9 @@ impl MpsController {
for msg in self.event.try_iter() {
Self::handle_event(msg)?;
}
self.control.send(ControlAction::CheckEmpty{ack: true}).map_err(PlaybackError::from_err)?;
self.control
.send(ControlAction::CheckEmpty { ack: true })
.map_err(PlaybackError::from_err)?;
loop {
let msg = self.event.recv().map_err(PlaybackError::from_err)?;
if let PlayerAction::Empty = msg {
@ -161,8 +173,12 @@ impl MpsController {
/// Like check(), but it also waits for an acknowledgement to ensure it gets the latest events.
pub fn check_ack(&self) -> Vec<PlaybackError> {
let mut result = Vec::new();
let to_send = ControlAction::NoOp{ack: true};
if let Err(e) = self.control.send(to_send.clone()).map_err(PlaybackError::from_err) {
let to_send = ControlAction::NoOp { ack: true };
if let Err(e) = self
.control
.send(to_send.clone())
.map_err(PlaybackError::from_err)
{
result.push(e);
}
for msg in self.event.iter() {
@ -171,7 +187,8 @@ impl MpsController {
break;
} else {
result.push(PlaybackError {
msg: "Incorrect acknowledgement received for MpsController control action".into()
msg: "Incorrect acknowledgement received for MpsController control action"
.into(),
});
}
} else if let Err(e) = Self::handle_event(msg) {

View file

@ -1,8 +1,8 @@
use std::fmt::{Debug, Display, Formatter, Error};
use std::fmt::{Debug, Display, Error, Formatter};
#[derive(Debug, Clone)]
pub struct PlaybackError {
pub(crate) msg: String
pub(crate) msg: String,
}
impl PlaybackError {

View file

@ -1,4 +1,6 @@
//! An MPS playback library with support for Linux media controls (D-Bus).
//! An MPS playback library with support for media controls (Linux & D-Bus only atm).
//! This handles the output from interpreting a script.
//! Music playback and m3u8 playlist generation are implemented in this part of the project.
//!
mod controller;

View file

@ -1,5 +1,5 @@
#[cfg(unix)]
use std::sync::mpsc::{Sender, channel};
use std::sync::mpsc::{channel, Sender};
#[cfg(unix)]
use std::thread::JoinHandle;
@ -9,10 +9,12 @@ use mpris_player::{MprisPlayer, PlaybackStatus};
//use super::MpsController;
use super::player_wrapper::ControlAction;
/// OS-specific APIs for media controls.
/// Currently only Linux (dbus) is supported.
pub struct SystemControlWrapper {
control: Sender<ControlAction>,
#[cfg(target_os = "linux")]
dbus_handle: Option<JoinHandle<()>>,//std::sync::Arc<MprisPlayer>,
dbus_handle: Option<JoinHandle<()>>, //std::sync::Arc<MprisPlayer>,
#[cfg(target_os = "linux")]
dbus_die: Option<Sender<()>>,
}
@ -22,7 +24,7 @@ impl SystemControlWrapper {
pub fn new(control: Sender<ControlAction>) -> Self {
Self {
control: control,
dbus_handle: None,//MprisPlayer::new("mps".into(), "mps".into(), "null".into())
dbus_handle: None, //MprisPlayer::new("mps".into(), "mps".into(), "null".into())
dbus_die: None,
}
}
@ -44,58 +46,67 @@ impl SystemControlWrapper {
dbus_conn.set_can_go_next(true);
let control_clone = control_clone1.clone();
dbus_conn.connect_next(
move || {
//println!("Got next signal");
control_clone.send(ControlAction::Next{ack: false}).unwrap_or(())
}
);
dbus_conn.connect_next(move || {
//println!("Got next signal");
control_clone
.send(ControlAction::Next { ack: false })
.unwrap_or(())
});
let control_clone = control_clone1.clone();
dbus_conn.connect_previous(
move || control_clone.send(ControlAction::Previous{ack: false}).unwrap_or(())
);
dbus_conn.connect_previous(move || {
control_clone
.send(ControlAction::Previous { ack: false })
.unwrap_or(())
});
let control_clone = control_clone1.clone();
let dbus_conn_clone = dbus_conn.clone();
dbus_conn.connect_pause(
move || {
//println!("Got pause signal");
dbus_conn.connect_pause(move || {
//println!("Got pause signal");
dbus_conn_clone.set_playback_status(PlaybackStatus::Paused);
control_clone
.send(ControlAction::Pause { ack: false })
.unwrap_or(());
});
let control_clone = control_clone1.clone();
let dbus_conn_clone = dbus_conn.clone();
dbus_conn.connect_play(move || {
//println!("Got play signal");
dbus_conn_clone.set_playback_status(PlaybackStatus::Playing);
control_clone
.send(ControlAction::Play { ack: false })
.unwrap_or(())
});
let control_clone = control_clone1.clone();
let dbus_conn_clone = dbus_conn.clone();
dbus_conn.connect_play_pause(move || {
//println!("Got play_pause signal (was playing? {})", is_playing);
if is_playing {
dbus_conn_clone.set_playback_status(PlaybackStatus::Paused);
control_clone.send(ControlAction::Pause{ack: false}).unwrap_or(());
}
);
let control_clone = control_clone1.clone();
let dbus_conn_clone = dbus_conn.clone();
dbus_conn.connect_play(
move || {
//println!("Got play signal");
control_clone
.send(ControlAction::Pause { ack: false })
.unwrap_or(());
} else {
dbus_conn_clone.set_playback_status(PlaybackStatus::Playing);
control_clone.send(ControlAction::Play{ack: false}).unwrap_or(())
control_clone
.send(ControlAction::Play { ack: false })
.unwrap_or(());
}
);
is_playing = !is_playing;
});
let control_clone = control_clone1.clone();
let dbus_conn_clone = dbus_conn.clone();
dbus_conn.connect_play_pause(
move || {
//println!("Got play_pause signal (was playing? {})", is_playing);
if is_playing {
dbus_conn_clone.set_playback_status(PlaybackStatus::Paused);
control_clone.send(ControlAction::Pause{ack: false}).unwrap_or(());
} else {
dbus_conn_clone.set_playback_status(PlaybackStatus::Playing);
control_clone.send(ControlAction::Play{ack: false}).unwrap_or(());
}
is_playing = !is_playing;
}
);
let control_clone = control_clone1.clone();
dbus_conn.connect_volume(
move |v| control_clone.send(ControlAction::SetVolume{ack: false, volume: (v * (u32::MAX as f64)) as _}).unwrap_or(())
);
dbus_conn.connect_volume(move |v| {
control_clone
.send(ControlAction::SetVolume {
ack: false,
volume: (v * (u32::MAX as f64)) as _,
})
.unwrap_or(())
});
// poll loop, using my custom mpris lib because original did it wrong
loop {
@ -120,9 +131,7 @@ impl SystemControlWrapper {
#[cfg(not(any(target_os = "linux")))]
impl SystemControlWrapper {
pub fn new(control: Sender<ControlAction>) -> Self {
Self {
control: control,
}
Self { control: control }
}
pub fn init(&mut self) {}

View file

@ -1,14 +1,16 @@
use std::io;
use std::fs;
use std::io;
use rodio::{decoder::Decoder, OutputStream, Sink, OutputStreamHandle};
use rodio::{decoder::Decoder, OutputStream, OutputStreamHandle, Sink};
use m3u8_rs::{MediaPlaylist, MediaSegment};
use mps_interpreter::{MpsRunner, tokens::MpsTokenReader};
use mps_interpreter::{tokens::MpsTokenReader, MpsRunner};
use super::PlaybackError;
/// Playback functionality for a script.
/// This takes the output of the runner and plays or saves it.
pub struct MpsPlayer<T: MpsTokenReader> {
runner: MpsRunner<T>,
sink: Sink,
@ -19,12 +21,13 @@ pub struct MpsPlayer<T: MpsTokenReader> {
impl<T: MpsTokenReader> MpsPlayer<T> {
pub fn new(runner: MpsRunner<T>) -> Result<Self, PlaybackError> {
let (stream, output_handle) = OutputStream::try_default().map_err(PlaybackError::from_err)?;
Ok(Self{
let (stream, output_handle) =
OutputStream::try_default().map_err(PlaybackError::from_err)?;
Ok(Self {
runner: runner,
sink: Sink::try_new(&output_handle).map_err(PlaybackError::from_err)?,
output_stream: stream,
output_handle: output_handle
output_handle: output_handle,
})
}
@ -39,8 +42,8 @@ impl<T: MpsTokenReader> MpsPlayer<T> {
self.sink.append(source);
//self.sink.play(); // idk if this is necessary
Ok(())
},
Err(e) => Err(PlaybackError::from_err(e))
}
Err(e) => Err(PlaybackError::from_err(e)),
}?;
}
self.sink.sleep_until_end();
@ -57,8 +60,8 @@ impl<T: MpsTokenReader> MpsPlayer<T> {
self.sink.append(source);
//self.sink.play(); // idk if this is necessary
Ok(())
},
Err(e) => Err(PlaybackError::from_err(e))
}
Err(e) => Err(PlaybackError::from_err(e)),
}?;
}
Ok(())
@ -66,7 +69,9 @@ impl<T: MpsTokenReader> MpsPlayer<T> {
pub fn enqueue(&mut self, count: usize) -> Result<(), PlaybackError> {
let mut items_left = count;
if items_left == 0 { return Ok(()); }
if items_left == 0 {
return Ok(());
}
for item in &mut self.runner {
match item {
Ok(music) => {
@ -77,11 +82,13 @@ impl<T: MpsTokenReader> MpsPlayer<T> {
self.sink.append(source);
//self.sink.play(); // idk if this is necessary
Ok(())
},
Err(e) => Err(PlaybackError::from_err(e))
}
Err(e) => Err(PlaybackError::from_err(e)),
}?;
items_left -= 1;
if items_left == 0 { break; }
if items_left == 0 {
break;
}
}
//println!("Enqueued {} items", count - items_left);
Ok(())
@ -120,16 +127,14 @@ impl<T: MpsTokenReader> MpsPlayer<T> {
for item in &mut self.runner {
match item {
Ok(music) => {
playlist.segments.push(
MediaSegment {
uri: music.filename,
title: Some(music.title),
..Default::default()
}
);
playlist.segments.push(MediaSegment {
uri: music.filename,
title: Some(music.title),
..Default::default()
});
Ok(())
},
Err(e) => Err(PlaybackError::from_err(e))
}
Err(e) => Err(PlaybackError::from_err(e)),
}?;
}
playlist.write_to(w).map_err(PlaybackError::from_err)
@ -160,9 +165,9 @@ impl<T: MpsTokenReader> MpsPlayer<T> {
#[cfg(test)]
mod tests {
use std::io;
use mps_interpreter::MpsRunner;
use super::*;
use mps_interpreter::MpsRunner;
use std::io;
#[allow(dead_code)]
#[test]

View file

@ -1,4 +1,4 @@
use std::sync::mpsc::{Sender, Receiver};
use std::sync::mpsc::{Receiver, Sender};
use std::{thread, thread::JoinHandle};
use mps_interpreter::tokens::MpsTokenReader;
@ -6,6 +6,10 @@ use mps_interpreter::tokens::MpsTokenReader;
use super::MpsPlayer;
use super::PlaybackError;
/// A wrapper around MpsPlayer so that playback can occur on a different thread.
/// This allows for message passing between the threads.
///
/// You will probably never directly interact with this, instead using MpsController to communicate.
pub struct MpsPlayerServer<T: MpsTokenReader> {
player: MpsPlayer<T>,
control: Receiver<ControlAction>,
@ -14,7 +18,12 @@ pub struct MpsPlayerServer<T: MpsTokenReader> {
}
impl<T: MpsTokenReader> MpsPlayerServer<T> {
pub fn new(player: MpsPlayer<T>, ctrl: Receiver<ControlAction>, event: Sender<PlayerAction>, keep_alive: bool) -> Self {
pub fn new(
player: MpsPlayer<T>,
ctrl: Receiver<ControlAction>,
event: Sender<PlayerAction>,
keep_alive: bool,
) -> Self {
Self {
player: player,
control: ctrl,
@ -39,7 +48,7 @@ impl<T: MpsTokenReader> MpsPlayerServer<T> {
// process command
match command {
ControlAction::Next{..} => {
ControlAction::Next { .. } => {
//println!("Executing next command (queue_len: {})", self.player.queue_len());
if let Err(e) = self.player.new_sink() {
self.event.send(PlayerAction::Exception(e)).unwrap();
@ -47,32 +56,32 @@ impl<T: MpsTokenReader> MpsPlayerServer<T> {
if !self.player.is_paused() {
self.player.enqueue(1).unwrap();
}
},
ControlAction::Previous{..} => {}, // TODO
ControlAction::Play{..} => self.player.resume(),
ControlAction::Pause{..} => self.player.pause(),
ControlAction::PlayPause{..} => {
}
ControlAction::Previous { .. } => {} // TODO
ControlAction::Play { .. } => self.player.resume(),
ControlAction::Pause { .. } => self.player.pause(),
ControlAction::PlayPause { .. } => {
if self.player.is_paused() {
self.player.resume();
} else {
self.player.pause();
}
},
ControlAction::Stop{..} => self.player.stop(),
ControlAction::Exit{..} => {
}
ControlAction::Stop { .. } => self.player.stop(),
ControlAction::Exit { .. } => {
self.player.stop();
is_exiting = true;
},
ControlAction::Enqueue{amount,..} => {
}
ControlAction::Enqueue { amount, .. } => {
if let Err(e) = self.player.enqueue(amount) {
self.event.send(PlayerAction::Exception(e)).unwrap();
}
},
ControlAction::NoOp{..} => {}, // empty by design
ControlAction::SetVolume{volume,..} => {
}
ControlAction::NoOp { .. } => {} // empty by design
ControlAction::SetVolume { volume, .. } => {
self.player.set_volume((volume as f32) / (u32::MAX as f32));
},
ControlAction::CheckEmpty{..} => {
}
ControlAction::CheckEmpty { .. } => {
check_empty = true;
}
}
@ -82,7 +91,8 @@ impl<T: MpsTokenReader> MpsPlayerServer<T> {
if let Err(e) = self.player.enqueue(1) {
self.event.send(PlayerAction::Exception(e)).unwrap();
}
if self.player.queue_len() == 0 { // no more music to add
if self.player.queue_len() == 0 {
// no more music to add
is_exiting = !self.keep_alive || is_exiting;
}
}
@ -92,10 +102,12 @@ impl<T: MpsTokenReader> MpsPlayerServer<T> {
}
// always check for empty state change
if self.player.queue_len() == 0 && !is_empty { // just became empty
if self.player.queue_len() == 0 && !is_empty {
// just became empty
is_empty = true;
self.event.send(PlayerAction::Empty).unwrap();
} else if self.player.queue_len() != 0 && is_empty { // just got filled
} else if self.player.queue_len() != 0 && is_empty {
// just got filled
is_empty = false;
}
@ -103,7 +115,9 @@ impl<T: MpsTokenReader> MpsPlayerServer<T> {
self.event.send(PlayerAction::Empty).unwrap();
}
if is_exiting { break; }
if is_exiting {
break;
}
}
println!("Exiting playback server");
self.event.send(PlayerAction::End).unwrap();
@ -114,7 +128,7 @@ impl<T: MpsTokenReader> MpsPlayerServer<T> {
ctrl_tx: Sender<ControlAction>,
ctrl_rx: Receiver<ControlAction>,
event: Sender<PlayerAction>,
keep_alive: bool
keep_alive: bool,
) -> JoinHandle<()> {
thread::spawn(move || Self::unblocking_timer_loop(ctrl_tx, 50));
thread::spawn(move || {
@ -127,7 +141,7 @@ impl<T: MpsTokenReader> MpsPlayerServer<T> {
pub fn unblocking_timer_loop(ctrl_tx: Sender<ControlAction>, sleep_ms: u64) {
let dur = std::time::Duration::from_millis(sleep_ms);
loop {
if let Err(_) = ctrl_tx.send(ControlAction::NoOp{ack: false}) {
if let Err(_) = ctrl_tx.send(ControlAction::NoOp { ack: false }) {
break;
}
thread::sleep(dur);
@ -139,33 +153,33 @@ impl<T: MpsTokenReader> MpsPlayerServer<T> {
#[allow(dead_code)]
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum ControlAction {
Next{ack: bool},
Previous{ack: bool},
Play{ack: bool},
Pause{ack: bool},
PlayPause{ack: bool},
Stop{ack: bool},
Exit{ack: bool},
Enqueue {amount: usize, ack: bool},
NoOp{ack: bool},
SetVolume{ack: bool, volume: u32},
CheckEmpty{ack: bool},
Next { ack: bool },
Previous { ack: bool },
Play { ack: bool },
Pause { ack: bool },
PlayPause { ack: bool },
Stop { ack: bool },
Exit { ack: bool },
Enqueue { amount: usize, ack: bool },
NoOp { ack: bool },
SetVolume { ack: bool, volume: u32 },
CheckEmpty { ack: bool },
}
impl ControlAction {
fn needs_ack(&self) -> bool {
*match self {
Self::Next{ack} => ack,
Self::Previous{ack} => ack,
Self::Play{ack} => ack,
Self::Pause{ack} => ack,
Self::PlayPause{ack} => ack,
Self::Stop{ack} => ack,
Self::Exit{ack} => ack,
Self::Enqueue{ack,..} => ack,
Self::NoOp{ack,..} => ack,
Self::SetVolume{ack,..} => ack,
Self::CheckEmpty{ack} => ack,
Self::Next { ack } => ack,
Self::Previous { ack } => ack,
Self::Play { ack } => ack,
Self::Pause { ack } => ack,
Self::PlayPause { ack } => ack,
Self::Stop { ack } => ack,
Self::Exit { ack } => ack,
Self::Enqueue { ack, .. } => ack,
Self::NoOp { ack, .. } => ack,
Self::SetVolume { ack, .. } => ack,
Self::CheckEmpty { ack } => ack,
}
}
}
@ -183,7 +197,7 @@ impl PlayerAction {
pub fn is_acknowledgement(&self) -> bool {
match self {
Self::Acknowledge(_) => true,
_ => false
_ => false,
}
}
}

View file

@ -1,5 +1,5 @@
use std::sync::mpsc::{channel, Sender, Receiver};
use std::io::{Read, Write, self};
use std::io::{self, Read, Write};
use std::sync::mpsc::{channel, Receiver, Sender};
pub struct ChannelWriter {
tx: Sender<u8>,
@ -29,34 +29,36 @@ pub struct ChannelReader {
}
impl Read for ChannelReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let mut count = 0;
if self.blocking {
for b in self.rx.iter() {
buf[count] = b;
count += 1;
if count >= buf.len() {break;}
if count >= buf.len() {
break;
}
}
} else {
for b in self.rx.try_iter() {
buf[count] = b;
count += 1;
if count >= buf.len() {break;}
if count >= buf.len() {
break;
}
}
}
Ok(count)
}
}
}
pub fn channel_io() -> (ChannelWriter, ChannelReader) {
let (sender, receiver) = channel();
(
ChannelWriter {
tx: sender,
},
ChannelWriter { tx: sender },
ChannelReader {
rx: receiver,
blocking: false,
}
},
)
}

View file

@ -1,4 +1,4 @@
use clap::{Parser};
use clap::Parser;
#[derive(Parser)]
#[clap(author, version)]
@ -17,7 +17,7 @@ pub struct CliArgs {
/// In REPL mode, the prompt to display
#[clap(long, default_value_t = String::from("|"))]
pub prompt: String
pub prompt: String,
}
pub fn parse() -> CliArgs {

50
src/help.rs Normal file
View file

@ -0,0 +1,50 @@
/// Standard help string containing usage information for MPS.
pub fn help() -> String {
"This language is all about iteration. Almost everything is an iterator or operates on iterators. By default, any operation which is not an assignment will cause the script runner to handle (play/save) the items which that statement contains.
To view the currently-supported operations, try ?functions and ?filters".to_string()
}
pub fn functions() -> String {
"FUNCTIONS (?functions)
Similar to most other languages: function_name(param1, param2, etc.)
sql_init(generate = true|false, folder = `path/to/music`)
Initialize the SQLite database connection using the provided parameters. This must be performed before any other database operation (otherwise the database will be connected with default settings).
sql(`SQL query here`)
Perform a raw SQLite query on the database which MPS auto-generates. An iterator of the results is returned.
song(`something`)
Retrieve all songs in the database with a title like something.
album(`something`)
Retrieve all songs in the database with an album title like something.
artist(`something`)
Retrieve all songs in the database with an artist name like something.
genre(`something`)
Retrieve all songs in the database with a genre title like something.
repeat(iterable, count)
Repeat the iterable count times, or infinite times if count is omitted.
files(folder = `path/to/music`, recursive = true|false, regex = `pattern`)
Retrieve all files from a folder, matching a regex pattern.".to_string()
}
pub fn filters() -> String {
"FILTERS (?filters)
Operations to reduce the items in an iterable: iterable.(filter)
field == something
field >= something
field > something
field <= something
field < something
Compare all items, keeping only those that match the condition.
[empty]
Matches all items".to_string()
}

View file

@ -1,18 +1,19 @@
//! An MPS program which plays music.
//! This doesn't do much yet, since mps-interpreter is still under construction.
//!
//! Future home of a MPS REPL for playing music ergonomically through a CLI.
//! This project implements the interpreter (mps-interpreter), music player (mps-player), and CLI interface for MPS (root).
//! The CLI interface includes a REPL for running scripts.
//! The REPL interactive mode also provides more details about using MPS through the `?help` command.
//!
mod channel_io;
mod cli;
mod help;
mod repl;
use std::io;
use std::path::PathBuf;
use mps_interpreter::MpsRunner;
use mps_player::{MpsPlayer, PlaybackError, MpsController};
use mps_player::{MpsController, MpsPlayer, PlaybackError};
#[allow(dead_code)]
fn play_cursor() -> Result<(), PlaybackError> {
@ -36,7 +37,7 @@ fn main() {
let player_builder = move || {
let script_reader = io::BufReader::new(
std::fs::File::open(&script_file2)
.expect(&format!("Abort: Cannot open file `{}`", &script_file2))
.expect(&format!("Abort: Cannot open file `{}`", &script_file2)),
);
let runner = MpsRunner::with_stream(script_reader);
let player = MpsPlayer::new(runner).unwrap();
@ -45,12 +46,14 @@ fn main() {
if let Some(playlist_file) = &args.playlist {
// generate playlist
let mut player = player_builder();
let mut writer = io::BufWriter::new(
std::fs::File::create(playlist_file)
.expect(&format!("Abort: Cannot create writeable file `{}`", playlist_file))
);
let mut writer = io::BufWriter::new(std::fs::File::create(playlist_file).expect(
&format!("Abort: Cannot create writeable file `{}`", playlist_file),
));
match player.save_m3u8(&mut writer) {
Ok(_) => println!("Succes: Finished playlist `{}` from script `{}`", playlist_file, script_file),
Ok(_) => println!(
"Succes: Finished playlist `{}` from script `{}`",
playlist_file, script_file
),
Err(e) => eprintln!("{}", e),
}
} else {
@ -64,6 +67,7 @@ fn main() {
} else {
// start REPL
println!("Welcome to MPS interactive mode!");
println!("Run ?help for usage instructions.");
println!("End a statement with ; to execute it.");
repl::repl(args)
}

View file

@ -1,12 +1,12 @@
//! Read, Execute, Print Loop functionality
use std::io::{self, Write, Read, Stdin};
use std::io::{self, Read, Stdin, Write};
use mps_interpreter::MpsRunner;
use mps_player::{MpsPlayer, MpsController};
use mps_player::{MpsController, MpsPlayer};
use super::cli::CliArgs;
use super::channel_io::{channel_io, ChannelWriter};
use super::cli::CliArgs;
struct ReplState {
stdin: Stdin,
@ -37,16 +37,17 @@ pub fn repl(args: CliArgs) {
if let Some(playlist_file) = &args.playlist {
println!("Playlist mode (output: `{}`)", playlist_file);
let mut player = player_builder();
let mut playlist_writer = io::BufWriter::new(
std::fs::File::create(playlist_file)
.expect(&format!("Abort: Cannot create writeable file `{}`", playlist_file))
);
let mut playlist_writer = io::BufWriter::new(std::fs::File::create(playlist_file).expect(
&format!("Abort: Cannot create writeable file `{}`", playlist_file),
));
read_loop(&args, &mut state, || {
match player.save_m3u8(&mut playlist_writer) {
Ok(_) => {},
Ok(_) => {}
Err(e) => eprintln!("{}", e.message()),
}
playlist_writer.flush().expect("Failed to flush playlist to file");
playlist_writer
.flush()
.expect("Failed to flush playlist to file");
});
} else {
println!("Playback mode (output: audio device)");
@ -54,7 +55,7 @@ pub fn repl(args: CliArgs) {
read_loop(&args, &mut state, || {
if args.wait {
match ctrl.wait_for_empty() {
Ok(_) => {},
Ok(_) => {}
Err(e) => eprintln!("{}", e.message()),
}
} else {
@ -67,23 +68,34 @@ pub fn repl(args: CliArgs) {
}
fn read_loop<F: FnMut()>(args: &CliArgs, state: &mut ReplState, mut execute: F) -> ! {
let mut read_buf: [u8;1] = [0];
let mut read_buf: [u8; 1] = [0];
prompt(&mut state.line_number, args);
loop {
read_buf[0] = 0;
while read_buf[0] == 0 {
// TODO: enable raw mode (char by char) reading of stdin
state.stdin.read(&mut read_buf).expect("Failed to read stdin");
state
.stdin
.read(&mut read_buf)
.expect("Failed to read stdin");
}
match read_buf[0] as char {
'\n' => {
state.statement_buf.push(read_buf[0]);
state.writer.write(state.statement_buf.as_slice())
.expect("Failed to write to MPS interpreter");
execute();
let statement_result = std::str::from_utf8(state.statement_buf.as_slice());
if statement_result.is_ok() && statement_result.unwrap().starts_with("?") {
repl_commands(statement_result.unwrap());
state.writer.write(&[';' as u8]).unwrap_or(0);
} else {
state
.writer
.write(state.statement_buf.as_slice())
.expect("Failed to write to MPS interpreter");
execute();
}
state.statement_buf.clear();
prompt(&mut state.line_number, args);
},
}
_ => state.statement_buf.push(read_buf[0]),
}
}
@ -95,3 +107,13 @@ fn prompt(line: &mut usize, args: &CliArgs) {
*line += 1;
std::io::stdout().flush().expect("Failed to flush stdout");
}
fn repl_commands(command_str: &str) {
let words: Vec<&str> = command_str.split(" ").map(|s| s.trim()).collect();
match words[0] {
"?help" => println!("{}", super::help::help()),
"?function" | "?functions" => println!("{}", super::help::functions()),
"?filter" | "?filters" => println!("{}", super::help::filters()),
_ => println!("Unknown command, try ?help"),
}
}