0.6.0: artwork support, new SwiftUI client, new protocol iteration

Drop of the "XOR encryption"

Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
This commit is contained in:
Ivan Bushchik 2024-07-12 23:25:36 +03:00
parent ff52e14863
commit ec55bd2b1e
No known key found for this signature in database
GPG key ID: 2F16FBF3262E090C
75 changed files with 2590 additions and 1015 deletions

1125
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,19 +5,19 @@ members = [
"monoclient-s", "monoclient-s",
"monolib", "monolib",
"monoloader", "monoloader",
"microserve",
] ]
[package] [package]
name = "lonelyradio" name = "lonelyradio"
description = "TCP radio for lonely ones" description = "TCP radio for lonely ones"
version = "0.5.0" version = "0.6.0"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"] authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
repository = "https://github.com/ivabus/lonelyradio" repository = "https://github.com/ivabus/lonelyradio"
[dependencies] [dependencies]
lonelyradio_types = { version = "0.6.0", path = "./lonelyradio_types" }
rand = "0.8.5" rand = "0.8.5"
clap = { version = "4.4.18", features = ["derive"] } clap = { version = "4.4.18", features = ["derive"] }
tokio = { version = "1.35.1", features = [ tokio = { version = "1.35.1", features = [
@ -43,9 +43,12 @@ async-stream = "0.3.5"
tokio-stream = { version = "0.1.15", features = ["sync"] } tokio-stream = { version = "0.1.15", features = ["sync"] }
futures-util = "0.3.30" futures-util = "0.3.30"
samplerate = "0.2.4" samplerate = "0.2.4"
lonelyradio_types = { version = "0.5.0", path = "./lonelyradio_types" }
once_cell = "1.19.0" once_cell = "1.19.0"
flacenc = { version = "0.4.0", default-features = false } flacenc = { version = "0.4.0", default-features = false }
image = "0.25.1"
[build-dependencies]
cc = "1.0.98"
[profile.release] [profile.release]
opt-level = 3 opt-level = 3

View file

@ -1,76 +1,90 @@
# lonelyradio # lonelyradio
Broadcast audio over the internet. Broadcast lossless audio over the internet.
Decodes audio streams using [symphonia](https://github.com/pdeljanov/Symphonia). Decodes audio streams using [symphonia](https://github.com/pdeljanov/Symphonia).
Optionally transcodes audio into and from FLAC using [flacenc-rs](https://github.com/yotarok/flacenc-rs/) and [claxon](https://github.com/ruuda/claxon). Optionally transcodes audio into and from FLAC using [flacenc-rs](https://github.com/yotarok/flacenc-rs/) and [claxon](https://github.com/ruuda/claxon).
## Installation ## Install server
### Install music server
```shell ```shell
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.5.0 lonelyradio cargo install --git https://github.com/ivabus/lonelyradio --tag 0.6.0 lonelyradio
```
### Install CLI client
```shell
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.5.0 monoclient
```
### Install GUI (Slint) client
```shell
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.5.0 monoclient-s
``` ```
## Run ## Run
``` ```
lonelyradio [-a <ADDRESS:PORT>] [-p|--public-log] [-w|--war] [-m|--max-samplerate M] [--xor-key-file FILE] [--no-resampling] [-f|--flac] <MUSIC_FOLDER> lonelyradio <MUSIC_FOLDER>
``` ```
All files (recursively) will be shuffled and played back. Public log will be displayed to stdout, private to stderr. All files (recursively) will be shuffled and played back. Public log will be displayed to stdout, private to stderr.
`-m|--max-samplerate M` will resample tracks which samplerate exceeds M to M Look into `--help` for detailed info
`--xor-key-file FILE` will XOR all outgoing bytes looping through FILE
`-f|--flac` will enable (experimental) FLAC compression
### Clients ### Clients
[monoclient](./monoclient) is a recommended CLI player for lonelyradio that uses [monolib](./monolib) #### monoclient-x
[monoclient-x](./monoclient-x) is a SwiftUI player for lonelyradio for iOS/iPadOS/macOS
##### Build
1. Build monolib with [xcframework](https://github.com/Binlogo/cargo-xcframework)
2. Build monoclient-x using Xcode or `xcodebuild`
#### monoclient-s
[monoclient-s](./monoclient-s) is a GUI player for lonelyradio built with [Slint](https://slint.dev)
##### Install
```shell
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.6.0 monoclient-s
```
You may need to install some dependencies for Slint.
Desktop integration will be added later.
##### Build
```
cargo build -p monoclient-s
```
You may need to install some dependencies for Slint.
#### monoclient
[monoclient](./monoclient) is a CLI player for lonelyradio that uses [monolib](./monolib)
```shell ```shell
monoclient <SERVER>:<PORT> monoclient <SERVER>:<PORT>
``` ```
[monoclient-s](./monoclient-s) is a experimental GUI player for lonelyradio built with [Slint](https://slint.dev) ##### Install monoclient
```shell ```shell
monoclient-s cargo install --git https://github.com/ivabus/lonelyradio --tag 0.6.0 monoclient
``` ```
Desktop integration will be added later. # Other things
### Other clients
SwiftUI client is availible in [platform](./platform) directory.
[monoloader](./monoloader) is a tool, that allows you to download individual audio tracks from lonelyradio-compatible servers. [monoloader](./monoloader) is a tool, that allows you to download individual audio tracks from lonelyradio-compatible servers.
[monolib](./monolib) provides a C API compatible with lonelyradio for creating custom clients. [monolib](./monolib) provides a C API compatible with lonelyradio for creating custom clients.
The full protocol specification will be available later. If you would like to learn more about it now, please refer to the monolib.
#### monolib API stability #### monolib API stability
As lonelyradio has not yet reached its first major release, the API may (and will) break at any point. As lonelyradio has not yet reached its first major release, the API may (and will) break at any point.
### Microphone server ### Microphone server
Experimental server (lonelyradio-compatible) for streaming audio from your microphone is available in the [microserve](./microserve) crate. Experimental (and uncompatible with versions 0.6+) server (lonelyradio-compatible) for streaming audio from your microphone is available in the [microserve](./microserve) crate.
## License ## License

View file

@ -2,10 +2,11 @@
name = "lonelyradio_types" name = "lonelyradio_types"
description = "Shared types for lonelyradio" description = "Shared types for lonelyradio"
license = "MIT" license = "MIT"
version = "0.5.0" version = "0.6.0"
edition = "2021" edition = "2021"
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"] authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
repository = "https://github.com/ivabus/lonelyradio" repository = "https://github.com/ivabus/lonelyradio"
[dependencies] [dependencies]
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }
serde_bytes = "0.11.15"

View file

@ -1,25 +1,63 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub const HELLO_MAGIC: u64 = 0x104e1374d10;
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] #[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub enum Message { pub enum Message {
T(TrackMetadata), T(TrackMetadata),
F(FragmentMetadata), F(FragmentMetadata),
} }
#[repr(C)]
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct Settings {
#[serde(rename = "e")]
pub encoder: Encoder,
#[serde(rename = "co")]
pub cover: i32,
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct ServerCapabilities {
#[serde(rename = "e")]
pub encoders: Vec<Encoder>,
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] #[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct TrackMetadata { pub struct TrackMetadata {
#[serde(rename = "tls")]
pub track_length_secs: u64, pub track_length_secs: u64,
#[serde(rename = "tlf")]
pub track_length_frac: f32, pub track_length_frac: f32,
#[serde(rename = "c")]
pub channels: u16, pub channels: u16,
#[serde(rename = "sr")]
pub sample_rate: u32, pub sample_rate: u32,
pub flac: bool, #[serde(rename = "e")]
pub encoder: Encoder,
#[serde(rename = "mt")]
pub title: String, pub title: String,
#[serde(rename = "mal")]
pub album: String, pub album: String,
#[serde(rename = "mar")]
pub artist: String, pub artist: String,
#[serde(rename = "co")]
#[serde(with = "serde_bytes")]
pub cover: Option<Vec<u8>>,
}
#[repr(u8)]
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq)]
pub enum Encoder {
Pcm16 = 0,
PcmFloat = 1,
Flac = 2,
} }
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] #[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct FragmentMetadata { pub struct FragmentMetadata {
// In samples or bytes (if FLAC) // In bytes
#[serde(rename = "l")]
pub length: u64, pub length: u64,
} }

View file

@ -39,7 +39,8 @@ async fn update_start() {
async fn stream(mut s: std::net::TcpStream) { async fn stream(mut s: std::net::TcpStream) {
println!("Playing for {}", s.peer_addr().unwrap()); println!("Playing for {}", s.peer_addr().unwrap());
let md = lonelyradio_types::Message::T(TrackMetadata { let md = lonelyradio_types::Message::T(TrackMetadata {
flac: false, cover: None,
encoder: lonelyradio_types::Encoder::Pcm,
track_length_secs: 0, track_length_secs: 0,
track_length_frac: 0.0, track_length_frac: 0.0,
channels: 1, channels: 1,

View file

@ -1,19 +1,22 @@
[package] [package]
name = "monoclient-s" name = "monoclient-s"
description = "Client for lonelyradio built with Slint" description = "Client for lonelyradio built with Slint"
version = "0.5.0" version = "0.6.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
slint = { version = "1.6.0", features = ["backend-android-activity-06"] } slint = { version = "1.6", features = ["backend-android-activity-06"] }
monolib = { path = "../monolib" } monolib = { path = "../monolib", version = "0.6.0" }
lonelyradio_types = { version = "0.6.0", path = "../lonelyradio_types" }
zune-jpeg = "0.4.11"
[lib]
crate-type = [ "cdylib" ]
# TODO: Set up cargo-bundle [package.metadata.bundle]
#[package.metadata.bundle] name = "monoclient-s"
#name = "monoclient-s" identifier = "dev.ivabus.monoclient-s"
#identifier = "dev.ivabus.monoclient-s" icon = ["lonelyradio.png", "lonelyradio.icns"]
#icon = ["lonelyradio.png", "lonelyradio.icns"] version = "0.5.0"
#version = "0.5.0" copyright = "Copyright (c) 2024 Ivan Bushchik."
#copyright = "Copyright (c) 2024 Ivan Bushchik." category = "Music"
#category = "Music"

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

227
monoclient-s/src/app.rs Normal file
View file

@ -0,0 +1,227 @@
use std::time::Duration;
use monolib::State;
use slint::{Image, Rgb8Pixel, Rgba8Pixel, SharedPixelBuffer, Weak};
slint::slint! {
import { AboutSlint, Button, VerticalBox, GroupBox, Slider } from "std-widgets.slint";
export component MainWindow inherits Window {
max-height: self.preferred-height;
callback play;
callback stop;
callback next;
callback change_volume(float);
callback text_edited;
in-out property <string> addr: address.text;
in-out property <string> mtitle: "";
in-out property <string> malbum: "";
in-out property <string> martist: "";
in-out property <float> volume: svolume.value;
in-out property <bool> start_enabled: false;
in-out property <bool> playing: false;
in-out property <bool> paused: false;
in property <image> cover: @image-url("lonelyradio.png");
title: "monoclient-s";
min-width: 192px;
max-width: 768px;
VerticalBox {
alignment: center;
spacing: 0px;
Image {
source: cover;
max-height: 192px;
max-width: 192px;
min-height: 192px;
min-width: 192px;
}
GroupBox{
max-width: 768px;
address := TextInput {
text: "";
horizontal-alignment: center;
height: 1.25rem;
accepted => {
self.clear_focus()
}
edited => {
text_edited()
}
}
}
VerticalLayout {
max-width: 512px;
VerticalLayout {
spacing: 4px;
Button {
max-width: 256px;
text: playing ? (paused ? "Play" : "Pause") : "Start";
enabled: start_enabled || playing;
clicked => {
play()
}
}
HorizontalLayout {
spacing: 4px;
max-width: 256px;
Button {
text: "Stop";
enabled: playing && !paused;
clicked => {
stop()
}
}
Button {
text: "Next";
enabled: playing && !paused;
clicked => {
next()
}
}
}
svolume := Slider {
value: 255;
maximum: 255;
changed(f) => {
change_volume(f)
}
}
}
}
VerticalLayout {
padding: 4px;
tartist := Text {
height: 1.25rem;
font-weight: 600;
text: martist;
overflow: elide;
}
talbum := Text {
height: 1.25rem;
text: malbum;
overflow: elide;
}
ttitle := Text {
height: 1.25rem;
text: mtitle;
overflow: elide;
}
}
}
}
}
fn start_playback(window_weak: Weak<MainWindow>) {
let window = window_weak.upgrade().unwrap();
let addr = window.get_addr().to_string();
let handle = std::thread::spawn(move || {
monolib::run(
&addr,
lonelyradio_types::Settings {
encoder: lonelyradio_types::Encoder::Flac,
cover: 512,
},
)
});
std::thread::sleep(Duration::from_millis(166));
if handle.is_finished() {
window.set_playing(false);
return;
}
window.set_playing(true);
window.set_paused(false);
while monolib::get_metadata().is_none() {}
monolib::set_volume(window.get_volume() as u8);
}
pub fn _main() {
let window = MainWindow::new().unwrap();
let window_weak = window.as_weak();
window.on_text_edited(move || {
let window = window_weak.upgrade().unwrap();
let addr = window.get_addr().to_string();
window.set_start_enabled(addr.contains(':'));
});
let window_weak = window.as_weak();
window.on_play(move || match monolib::get_state() {
State::NotStarted => start_playback(window_weak.clone()),
State::Paused => {
let window = window_weak.upgrade().unwrap();
window.set_paused(false);
monolib::toggle();
}
State::Resetting => {}
State::Playing => {
let window = window_weak.upgrade().unwrap();
window.set_paused(true);
monolib::toggle()
}
});
let window_weak = window.as_weak();
window.on_next(move || {
monolib::stop();
start_playback(window_weak.clone())
});
let window_weak = window.as_weak();
window.on_stop(move || {
let window = window_weak.upgrade().unwrap();
window.set_playing(false);
window.set_martist("".into());
window.set_malbum("".into());
window.set_mtitle("".into());
window.set_cover(Image::from_rgba8(SharedPixelBuffer::<Rgba8Pixel>::new(1, 1)));
monolib::stop();
});
window.on_change_volume(move |vol| monolib::set_volume(vol as u8));
let window_weak = window.as_weak();
std::thread::spawn(move || loop {
let window = window_weak.clone();
while monolib::get_metadata().is_none() {
std::thread::sleep(Duration::from_millis(25))
}
let md = monolib::get_metadata().unwrap();
let _md = md.clone();
if let Some(jpeg) = md.cover {
let mut decoder = zune_jpeg::JpegDecoder::new(jpeg);
decoder.decode_headers().unwrap();
let (w, h) = decoder.dimensions().unwrap();
let decoded = decoder.decode().unwrap();
let mut pixel_buffer = SharedPixelBuffer::<Rgb8Pixel>::new(w as u32, h as u32);
pixel_buffer.make_mut_bytes().copy_from_slice(&decoded);
window
.upgrade_in_event_loop(|win| {
let image = Image::from_rgb8(pixel_buffer);
win.set_cover(image);
})
.unwrap();
} else {
window
.upgrade_in_event_loop(|win| {
win.set_cover(Image::from_rgba8(SharedPixelBuffer::<Rgba8Pixel>::new(1, 1)));
})
.unwrap();
}
slint::invoke_from_event_loop(move || {
let window = window.unwrap();
window.set_martist(md.artist.clone().into());
window.set_malbum(md.album.clone().into());
window.set_mtitle(md.title.clone().into());
})
.unwrap();
while monolib::get_metadata() == Some(_md.clone()) {
std::thread::sleep(Duration::from_millis(100))
}
});
window.run().unwrap();
}

7
monoclient-s/src/lib.rs Normal file
View file

@ -0,0 +1,7 @@
mod app;
#[cfg(target_os = "andoid")]
#[no_mangle]
fn android_main(app: slint::android::AndroidApp) {
slint::android::init(app).unwrap();
app::_main();
}

View file

@ -1,181 +1,4 @@
use std::time::Duration; mod app;
fn main() {
use monolib::State; app::_main()
use slint::Weak;
slint::slint! {
import { AboutSlint, Button, VerticalBox, GroupBox, Slider } from "std-widgets.slint";
export component MainWindow inherits Window {
max-height: self.preferred-height;
callback play;
callback stop;
callback next;
callback change_volume(float);
callback text_edited;
in-out property <string> addr: address.text;
in-out property <string> mtitle: "";
in-out property <string> malbum: "";
in-out property <string> martist: "";
in-out property <float> volume: svolume.value;
in-out property <bool> start_enabled: false;
in-out property <bool> playing: false;
in-out property <bool> paused: false;
title: "monoclient-s";
min-width: 192px;
max-width: 768px;
VerticalBox {
alignment: center;
GroupBox{
max-width: 768px;
address := TextInput {
text: "";
horizontal-alignment: center;
height: 1.25rem;
accepted => {
self.clear_focus()
}
edited => {
text_edited()
}
}
}
VerticalLayout {
max-width: 512px;
VerticalLayout {
spacing: 4px;
Button {
max-width: 256px;
text: playing ? (paused ? "Play" : "Pause") : "Start";
enabled: start_enabled || playing;
clicked => {
play()
}
}
HorizontalLayout {
spacing: 4px;
max-width: 256px;
Button {
text: "Stop";
enabled: playing && !paused;
clicked => {
stop()
}
}
Button {
text: "Next";
enabled: playing && !paused;
clicked => {
next()
}
}
}
svolume := Slider {
value: 255;
maximum: 255;
changed(f) => {
change_volume(f)
}
}
}
tartist := Text {
height: 1.25rem;
font-weight: 600;
text: martist;
overflow: elide;
}
talbum := Text {
height: 1.25rem;
text: malbum;
overflow: elide;
}
ttitle := Text {
height: 1.25rem;
text: mtitle;
overflow: elide;
}
}
}
}
}
fn start_playback(window_weak: Weak<MainWindow>) {
let window = window_weak.upgrade().unwrap();
let addr = window.get_addr().to_string();
let handle = std::thread::spawn(move || monolib::run(&addr, None));
std::thread::sleep(Duration::from_millis(166));
if handle.is_finished() {
window.set_playing(false);
return;
}
window.set_playing(true);
window.set_paused(false);
while monolib::get_metadata().is_none() {}
monolib::set_volume(window.get_volume() as u8);
}
pub fn main() {
let window = MainWindow::new().unwrap();
let window_weak = window.as_weak();
window.on_text_edited(move || {
let window = window_weak.upgrade().unwrap();
let addr = window.get_addr().to_string();
window.set_start_enabled(addr.contains(':'));
});
let window_weak = window.as_weak();
window.on_play(move || match monolib::get_state() {
State::NotStarted => start_playback(window_weak.clone()),
State::Paused => {
let window = window_weak.upgrade().unwrap();
window.set_paused(false);
monolib::toggle();
}
State::Resetting => {}
State::Playing => {
let window = window_weak.upgrade().unwrap();
window.set_paused(true);
monolib::toggle()
}
});
let window_weak = window.as_weak();
window.on_next(move || {
monolib::stop();
start_playback(window_weak.clone())
});
let window_weak = window.as_weak();
window.on_stop(move || {
let window = window_weak.upgrade().unwrap();
window.set_playing(false);
window.set_martist("".into());
window.set_malbum("".into());
window.set_mtitle("".into());
monolib::stop();
});
window.on_change_volume(move |vol| monolib::set_volume(vol as u8));
let window_weak = window.as_weak();
std::thread::spawn(move || loop {
let window = window_weak.clone();
while monolib::get_metadata().is_none() {
std::thread::sleep(Duration::from_millis(25))
}
let md = monolib::get_metadata().unwrap();
let _md = md.clone();
slint::invoke_from_event_loop(move || {
let window = window.unwrap();
window.set_martist(md.artist.clone().into());
window.set_malbum(md.album.clone().into());
window.set_mtitle(md.title.clone().into());
})
.unwrap();
while monolib::get_metadata() == Some(_md.clone()) {
std::thread::sleep(Duration::from_millis(100))
}
});
window.run().unwrap();
} }

View file

@ -0,0 +1,453 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 60;
objects = {
/* Begin PBXBuildFile section */
4F79BFA42C19977F00074B09 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F79BFA32C19975000074B09 /* libresolv.tbd */; };
4F92D0562C4176A200CF3363 /* MonoLib.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F92D0552C4176A200CF3363 /* MonoLib.xcframework */; };
4F92D0572C4176A200CF3363 /* MonoLib.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4F92D0552C4176A200CF3363 /* MonoLib.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
4FAD30F72C1980D900074B09 /* monoclient_xApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FAD30F62C1980D900074B09 /* monoclient_xApp.swift */; };
4FAD30F92C1980D900074B09 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FAD30F82C1980D900074B09 /* ContentView.swift */; };
4FAD30FB2C1980D900074B09 /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FAD30FA2C1980D900074B09 /* Metadata.swift */; };
4FAD30FD2C1980DC00074B09 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4FAD30FC2C1980DC00074B09 /* Assets.xcassets */; };
4FAD31012C1980DC00074B09 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4FAD31002C1980DC00074B09 /* Preview Assets.xcassets */; };
4FAE6E662C1B5EB100074B09 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FAE6E652C1B5EB100074B09 /* Player.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
4F15BE3A2C3AF1840026AC81 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
4FF5DF8A2C41575B0039B22C /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
4F92D0572C4176A200CF3363 /* MonoLib.xcframework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
4F15BE242C3AF1810026AC81 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
4F79BF922C19903C00074B09 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
4F79BFA32C19975000074B09 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/usr/lib/libresolv.tbd; sourceTree = DEVELOPER_DIR; };
4F92D0552C4176A200CF3363 /* MonoLib.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MonoLib.xcframework; path = ../target/MonoLib.xcframework; sourceTree = "<group>"; };
4FAD30F32C1980D900074B09 /* monoclient-x.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "monoclient-x.app"; sourceTree = BUILT_PRODUCTS_DIR; };
4FAD30F62C1980D900074B09 /* monoclient_xApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = monoclient_xApp.swift; sourceTree = "<group>"; };
4FAD30F82C1980D900074B09 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
4FAD30FA2C1980D900074B09 /* Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metadata.swift; sourceTree = "<group>"; };
4FAD30FC2C1980DC00074B09 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
4FAD30FE2C1980DC00074B09 /* monoclient_x.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = monoclient_x.entitlements; sourceTree = "<group>"; };
4FAD31002C1980DC00074B09 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
4FAE6E652C1B5EB100074B09 /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
4FAD30F02C1980D900074B09 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
4F92D0562C4176A200CF3363 /* MonoLib.xcframework in Frameworks */,
4F79BFA42C19977F00074B09 /* libresolv.tbd in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
4F79BF942C1992ED00074B09 /* Frameworks */ = {
isa = PBXGroup;
children = (
4F92D0552C4176A200CF3363 /* MonoLib.xcframework */,
4F79BFA32C19975000074B09 /* libresolv.tbd */,
4F15BE242C3AF1810026AC81 /* SwiftUI.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
4FAD30EA2C1980D900074B09 = {
isa = PBXGroup;
children = (
4FAD30F52C1980D900074B09 /* monoclient-x */,
4FAD30F42C1980D900074B09 /* Products */,
4F79BF942C1992ED00074B09 /* Frameworks */,
);
sourceTree = "<group>";
};
4FAD30F42C1980D900074B09 /* Products */ = {
isa = PBXGroup;
children = (
4FAD30F32C1980D900074B09 /* monoclient-x.app */,
);
name = Products;
sourceTree = "<group>";
};
4FAD30F52C1980D900074B09 /* monoclient-x */ = {
isa = PBXGroup;
children = (
4F79BF922C19903C00074B09 /* Info.plist */,
4FAD30F62C1980D900074B09 /* monoclient_xApp.swift */,
4FAD30F82C1980D900074B09 /* ContentView.swift */,
4FAD30FA2C1980D900074B09 /* Metadata.swift */,
4FAD30FC2C1980DC00074B09 /* Assets.xcassets */,
4FAD30FE2C1980DC00074B09 /* monoclient_x.entitlements */,
4FAD30FF2C1980DC00074B09 /* Preview Content */,
4FAE6E652C1B5EB100074B09 /* Player.swift */,
);
path = "monoclient-x";
sourceTree = "<group>";
};
4FAD30FF2C1980DC00074B09 /* Preview Content */ = {
isa = PBXGroup;
children = (
4FAD31002C1980DC00074B09 /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
4FAD30F22C1980D900074B09 /* monoclient-x */ = {
isa = PBXNativeTarget;
buildConfigurationList = 4FAD31042C1980DC00074B09 /* Build configuration list for PBXNativeTarget "monoclient-x" */;
buildPhases = (
4FAD30EF2C1980D900074B09 /* Sources */,
4FAD30F02C1980D900074B09 /* Frameworks */,
4FAD30F12C1980D900074B09 /* Resources */,
4F15BE3A2C3AF1840026AC81 /* Embed Foundation Extensions */,
4FF5DF8A2C41575B0039B22C /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = "monoclient-x";
packageProductDependencies = (
);
productName = "monoclient-x";
productReference = 4FAD30F32C1980D900074B09 /* monoclient-x.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
4FAD30EB2C1980D900074B09 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1600;
LastUpgradeCheck = 1600;
TargetAttributes = {
4FAD30F22C1980D900074B09 = {
CreatedOnToolsVersion = 16.0;
};
};
};
buildConfigurationList = 4FAD30EE2C1980D900074B09 /* Build configuration list for PBXProject "monoclient-x" */;
compatibilityVersion = "Xcode 15.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 4FAD30EA2C1980D900074B09;
packageReferences = (
);
productRefGroup = 4FAD30F42C1980D900074B09 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
4FAD30F22C1980D900074B09 /* monoclient-x */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
4FAD30F12C1980D900074B09 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4FAD31012C1980DC00074B09 /* Preview Assets.xcassets in Resources */,
4FAD30FD2C1980DC00074B09 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
4FAD30EF2C1980D900074B09 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4FAE6E662C1B5EB100074B09 /* Player.swift in Sources */,
4FAD30F92C1980D900074B09 /* ContentView.swift in Sources */,
4FAD30FB2C1980D900074B09 /* Metadata.swift in Sources */,
4FAD30F72C1980D900074B09 /* monoclient_xApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
4FAD31022C1980DC00074B09 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
4FAD31032C1980DC00074B09 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SWIFT_COMPILATION_MODE = wholemodule;
};
name = Release;
};
4FAD31052C1980DC00074B09 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_USE_OPTIMIZATION_PROFILE = YES;
CODE_SIGN_ENTITLEMENTS = "monoclient-x/monoclient_x.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"monoclient-x/Preview Content\"";
DEVELOPMENT_TEAM = F5PQ7AR4DP;
ENABLE_HARDENED_RUNTIME = YES;
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = "${SRCROOT}/../target/**";
GENERATE_INFOPLIST_FILE = YES;
HEADER_SEARCH_PATHS = "";
INFOPLIST_FILE = "monoclient-x/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "monoclient-x";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.music";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
LIBRARY_SEARCH_PATHS = "";
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 0.6.0;
PRODUCT_BUNDLE_IDENTIFIER = "dev.ivabus.monoclient-x";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
XROS_DEPLOYMENT_TARGET = 2.0;
};
name = Debug;
};
4FAD31062C1980DC00074B09 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_USE_OPTIMIZATION_PROFILE = YES;
CODE_SIGN_ENTITLEMENTS = "monoclient-x/monoclient_x.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"monoclient-x/Preview Content\"";
DEVELOPMENT_TEAM = F5PQ7AR4DP;
ENABLE_HARDENED_RUNTIME = YES;
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = "${SRCROOT}/../target/**";
GENERATE_INFOPLIST_FILE = YES;
HEADER_SEARCH_PATHS = "";
INFOPLIST_FILE = "monoclient-x/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "monoclient-x";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.music";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
LIBRARY_SEARCH_PATHS = "${PROJECT_DIR/../target/aarch64-apple-darwin/release}";
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 0.6.0;
PRODUCT_BUNDLE_IDENTIFIER = "dev.ivabus.monoclient-x";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
XROS_DEPLOYMENT_TARGET = 2.0;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
4FAD30EE2C1980D900074B09 /* Build configuration list for PBXProject "monoclient-x" */ = {
isa = XCConfigurationList;
buildConfigurations = (
4FAD31022C1980DC00074B09 /* Debug */,
4FAD31032C1980DC00074B09 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
4FAD31042C1980DC00074B09 /* Build configuration list for PBXNativeTarget "monoclient-x" */ = {
isa = XCConfigurationList;
buildConfigurations = (
4FAD31052C1980DC00074B09 /* Debug */,
4FAD31062C1980DC00074B09 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 4FAD30EB2C1980D900074B09 /* Project object */;
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View file

@ -2,9 +2,7 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>com.apple.security.app-sandbox</key> <key>IDEDidComputeMac32BitWarning</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/> <true/>
</dict> </dict>
</plist> </plist>

View file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4FAD30F22C1980D900074B09"
BuildableName = "monoclient-x.app"
BlueprintName = "monoclient-x"
ReferencedContainer = "container:monoclient-x.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4FAD30F22C1980D900074B09"
BuildableName = "monoclient-x.app"
BlueprintName = "monoclient-x"
ReferencedContainer = "container:monoclient-x.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4FAD30F22C1980D900074B09"
BuildableName = "monoclient-x.app"
BlueprintName = "monoclient-x"
ReferencedContainer = "container:monoclient-x.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -2,8 +2,8 @@
"colors" : [ "colors" : [
{ {
"color" : { "color" : {
"platform" : "ios", "platform" : "universal",
"reference" : "systemPinkColor" "reference" : "systemPurpleColor"
}, },
"idiom" : "universal" "idiom" : "universal"
} }

View file

@ -1,7 +1,31 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "icon-1024@1x.png", "filename" : "monoclient-x-ios.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "monoclient-x-ios 1.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"filename" : "monoclient-x-ios 2.png",
"idiom" : "universal", "idiom" : "universal",
"platform" : "ios", "platform" : "ios",
"size" : "1024x1024" "size" : "1024x1024"
@ -49,19 +73,19 @@
"size" : "256x256" "size" : "256x256"
}, },
{ {
"filename" : "icon-256@2x.png", "filename" : "Icon-macOS-512x512@1x 1.png",
"idiom" : "mac", "idiom" : "mac",
"scale" : "2x", "scale" : "2x",
"size" : "256x256" "size" : "256x256"
}, },
{ {
"filename" : "icon-512@1x.png", "filename" : "Icon-macOS-512x512@1x.png",
"idiom" : "mac", "idiom" : "mac",
"scale" : "1x", "scale" : "1x",
"size" : "512x512" "size" : "512x512"
}, },
{ {
"filename" : "icon-512@2x.png", "filename" : "Icon-macOS-512x512@2x.png",
"idiom" : "mac", "idiom" : "mac",
"scale" : "2x", "scale" : "2x",
"size" : "512x512" "size" : "512x512"

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View file

@ -0,0 +1,71 @@
//
// ContentView.swift
// monoclient-x
//
// Created by ivabus on 12.06.2024.
//
import SwiftUI
import SwiftData
import AppIntents
struct ContentView: View {
var body: some View {
Player()
}
}
struct PlayIntent: AudioPlaybackIntent {
static var title: LocalizedStringResource = "Start lonelyradio client"
static var description = IntentDescription("Plays from setted up server")
static var openAppWhenRun: Bool = false
static var isDiscoverable: Bool = true
@MainActor
func perform() async throws -> some IntentResult {
Player().play()
return .result()
}
}
struct StopIntent: AudioPlaybackIntent {
static var title: LocalizedStringResource = "Stop lonelyradio client"
static var description = IntentDescription("Stops monoclient")
static var openAppWhenRun: Bool = false
static var isDiscoverable: Bool = true
@MainActor
func perform() async throws -> some IntentResult {
Player().stop()
return .result()
}
}
struct LibraryAppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: PlayIntent(),
phrases: [
"Start playback \(.applicationName)",
],
shortTitle: "Start monoclient",
systemImageName: "infinity.circle"
)
AppShortcut(
intent: StopIntent(),
phrases: [
"Stop playback in \(.applicationName)"
],
shortTitle: "Stop monoclient",
systemImageName: "stop.fill"
)
}
}
#Preview {
ContentView()
}

View file

@ -0,0 +1,57 @@
//
// Item.swift
// monoclient-x
//
// Created by ivabus on 12.06.2024.
//
import CoreGraphics
import CoreFoundation
import SwiftUI
import MonoLib
#if os(macOS)
typealias PlatformImage = NSImage
#else
typealias PlatformImage = UIImage
#endif
struct Metadata {
public var title: String
public var album: String
public var artist: String
mutating func update() {
self.title = String(cString: c_get_metadata_title())
self.album = String(cString: c_get_metadata_album())
self.artist = String(cString: c_get_metadata_artist())
}
}
extension Metadata: Equatable {
static func == (lhs: Self, rhs: Self) -> Bool {
(lhs.album == rhs.album) && (lhs.artist == rhs.artist) && (lhs.title == rhs.title)
}
}
struct Cover {
public var cover: PlatformImage
mutating func update() {
let cov = c_get_cover_jpeg()
if cov.length != 0 {
let data = CFDataCreate(kCFAllocatorDefault, cov.bytes, Int(cov.length))!
#if os(macOS)
self.cover = PlatformImage(cgImage: CGImage(jpegDataProviderSource: CGDataProvider(data: data)!, decode: nil, shouldInterpolate: false, intent: CGColorRenderingIntent.absoluteColorimetric)!, size: NSSize.init(width: 768, height:768))
#else
self.cover = PlatformImage(cgImage: CGImage(jpegDataProviderSource: CGDataProvider(data: data)!, decode: nil, shouldInterpolate: false, intent: CGColorRenderingIntent.absoluteColorimetric)!).preparingForDisplay()!
#endif
// deallocating memory
c_drop(cov.bytes, Int(cov.length))
print(self.cover.size)
} else {
self.cover = PlatformImage()
}
}
}

View file

@ -0,0 +1,271 @@
//
// Player.swift
// monoclient-x
//
// Created by ivabus on 13.06.2024.
//
import SwiftUI
import AVFAudio
import MediaPlayer
import MonoLib
enum PlayerState {
case NotStarted
case Playing
case Paused
mutating func update() {
self = switch c_get_state() {
case 2: PlayerState.Playing
case 3: PlayerState.Paused
default: PlayerState.NotStarted
}
}
}
enum EncoderType: UInt8 {
case PCM16 = 0
case PCMFloat = 1
case FLAC = 2
}
enum CoverSize: Int32 {
case Full = 0
case High = 768
case Medium = 512
case Low = 256
case Min = 128
case NoCover = -1
}
struct Settings {
var encoder: EncoderType = EncoderType.FLAC
var cover_size: CoverSize = CoverSize.High/*
init(enc: EncoderType, cov: CoverSize) {
encoder = enc
cover_size = cov
}*/
}
struct Player: View {
let timer_state = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()
let timer_meta = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
@State var metadata: Metadata = Metadata(title: "", album: "", artist: "")
@State var prev_meta: Metadata = Metadata(title: "", album: "", artist: "")
@State var cover: Cover = Cover(cover: PlatformImage())
@State var state: PlayerState = PlayerState.NotStarted
@State var settings: Settings = Settings.init()
@AppStorage("ContentView.server") var server: String = ""
var body: some View {
VStack(alignment: .center) {
#if os(macOS)
Image(nsImage: cover.cover)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
.frame(width: 256.0, height: 256.0)
.clipShape(.rect(cornerRadius: 24))
.shadow(radius: 16)
.padding(16)
#else
Image(uiImage: cover.cover)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
.frame(width: 256.0, height: 256.0)
.clipShape(.rect(cornerRadius: 24))
.shadow(radius: 16)
.padding(16)
#endif
VStack(alignment: .center){
Text(metadata.title).bold()
Text(metadata.album)
Text(metadata.artist)
}.frame(minHeight: 64)
TextField(
"Server",
text: $server,
onCommit: {
#if os(macOS)
DispatchQueue.main.async {
NSApp.keyWindow?.makeFirstResponder(nil)
}
#endif
}
)
.disableAutocorrection(true)
.frame(width: 256)
.textFieldStyle(.roundedBorder)
.padding(16)
.multilineTextAlignment(.center)
HStack(spacing: 8) {
Button(action: stop){
Image(systemName: "stop.fill").padding(4).frame(width: 32, height: 24)
}
.disabled(state == PlayerState.NotStarted)
.buttonStyle(.bordered)
.font(.system(size: 20))
.buttonBorderShape(.capsule)
Button(action: play){
Image(systemName: state == PlayerState.NotStarted ? "infinity.circle" : (state == PlayerState.Playing) ? "pause.circle.fill" : "play.circle" )
.font(.system(size: 30))
.padding(4)
}
.buttonStyle(.borderedProminent)
.buttonBorderShape(.capsule)
Button(action: next){
Image(systemName: "forward.end.fill").padding(4).frame(width: 32, height: 24)
}.disabled(state == PlayerState.NotStarted)
.buttonStyle(.bordered)
.font(.system(size: 20))
.buttonBorderShape(.capsule)
}
Menu {
Picker("Encoder", selection: $settings.encoder) {
Text("PCM (s16)")
.tag(EncoderType.PCM16)
Text("PCM (f32)")
.tag(EncoderType.PCMFloat)
Text("FLAC (s24)")
.tag(EncoderType.FLAC)
}.pickerStyle(.menu)
Picker("Cover size", selection: $settings.cover_size) {
Text("Original")
.tag(CoverSize.Full)
Text("High (768)")
.tag(CoverSize.High)
Text("Medium (512)")
.tag(CoverSize.Medium)
Text("Low (256)")
.tag(CoverSize.Low)
Text("Min (128)")
.tag(CoverSize.Min)
Text("No cover")
.tag(CoverSize.NoCover)
}.pickerStyle(.menu)
} label: {
Label("Settings", systemImage: "gearshape")
.padding(16)
}.frame(maxWidth: 128)
}
.padding(32)
.onReceive(timer_state) { _ in
state.update()
#if os(macOS)
MPNowPlayingInfoCenter.default().playbackState = state == PlayerState.Playing ? .playing : .paused
#endif
}
.onReceive(timer_meta) { _ in
metadata.update()
if prev_meta != metadata || metadata.album == "" || cover.cover == PlatformImage() {
prev_meta = metadata
cover.update()
}
let image = cover.cover
let mediaArtwork = MPMediaItemArtwork(boundsSize: image.size) { (size: CGSize) -> PlatformImage in
return image
}
let nowPlayingInfo: [String: Any] = [
MPMediaItemPropertyArtist: metadata.artist,
MPMediaItemPropertyAlbumTitle: metadata.album,
MPMediaItemPropertyTitle: metadata.title,
MPMediaItemPropertyArtwork: mediaArtwork,
MPNowPlayingInfoPropertyIsLiveStream: true,
MPMediaItemPropertyPlaybackDuration: c_get_metadata_length(),
]
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
.onAppear() {
#if os(iOS)
UIApplication.shared.beginReceivingRemoteControlEvents()
#endif
MPRemoteCommandCenter.shared().previousTrackCommand.isEnabled = false
MPRemoteCommandCenter.shared().nextTrackCommand.isEnabled = true
MPRemoteCommandCenter.shared().skipForwardCommand.isEnabled = false
MPRemoteCommandCenter.shared().skipBackwardCommand.isEnabled = false
MPRemoteCommandCenter.shared().pauseCommand.addTarget(handler: { _ in
if state != PlayerState.Paused {
play()
}
return MPRemoteCommandHandlerStatus.success
})
MPRemoteCommandCenter.shared().playCommand.addTarget(handler: { _ in
if state != PlayerState.Playing {
play()
}
return MPRemoteCommandHandlerStatus.success
})
MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget(handler: {_ in
play()
return MPRemoteCommandHandlerStatus.success
})
MPRemoteCommandCenter.shared().nextTrackCommand.addTarget(handler: {_ in
next()
return MPRemoteCommandHandlerStatus.success
})
}
.animation(.spring, value: UUID())
}
func play() {
switch state {
case PlayerState.NotStarted: do {
#if os(iOS)
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(
.playback, mode: .default)
try audioSession.setActive(true)
} catch {
print("Failed to set the audio session configuration")
}
#endif
Thread.detachNewThread {
c_start(server, CSettings(encoder: settings.encoder.rawValue, cover: settings.cover_size.rawValue))
}
}
default: do {
c_toggle()
state.update()
}
}
}
func stop() {
c_stop()
state.update()
cover = Cover(cover: PlatformImage())
}
func next() {
c_stop()
state.update()
play()
}
}

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,35 @@
//
// monoclient_xApp.swift
// monoclient-x
//
// Created by ivabus on 12.06.2024.
//
import SwiftUI
@main
struct monoclient_xApp: App {
var body: some Scene {
#if os(macOS)
WindowGroup {
ContentView().onAppear {
NSWindow.allowsAutomaticWindowTabbing = false
}
.containerBackground(.ultraThinMaterial, for: .window)
.windowFullScreenBehavior(.disabled)
.windowResizeBehavior(.disabled)
}.defaultSize(width: 256, height: 512)
.windowStyle(.hiddenTitleBar)
.commands {
CommandGroup(replacing: CommandGroupPlacement.newItem) {
}
}
#else
WindowGroup {
ContentView()
}
.defaultSize(width: 256, height: 512)
#endif
}
}

View file

@ -1,12 +1,14 @@
[package] [package]
name = "monoclient" name = "monoclient"
license = "MIT" license = "MIT"
version = "0.5.0" version = "0.6.0"
edition = "2021" edition = "2021"
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"] authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
repository = "https://github.com/ivabus/lonelyradio" repository = "https://github.com/ivabus/lonelyradio"
[dependencies] [dependencies]
monolib = { version = "0.5.0", path = "../monolib" } monolib = { version = "0.6.0", path = "../monolib" }
clap = { version = "4.4.18", features = ["derive"] } clap = { version = "4.4.18", features = ["derive"] }
crossterm = "0.27.0" crossterm = "0.27.0"
lonelyradio_types = { version = "0.6.0", path = "../lonelyradio_types" }

View file

@ -3,10 +3,14 @@ use crossterm::cursor::MoveToColumn;
use crossterm::event::{poll, read, Event}; use crossterm::event::{poll, read, Event};
use crossterm::style::Print; use crossterm::style::Print;
use crossterm::terminal::{Clear, ClearType}; use crossterm::terminal::{Clear, ClearType};
use lonelyradio_types::{Encoder, Settings};
use std::io::stdout; use std::io::stdout;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::OnceLock;
use std::time::Instant; use std::time::Instant;
static VERBOSE: OnceLock<bool> = OnceLock::new();
#[derive(Parser)] #[derive(Parser)]
struct Args { struct Args {
/// Remote address /// Remote address
@ -14,6 +18,9 @@ struct Args {
#[arg(long)] #[arg(long)]
xor_key_file: Option<PathBuf>, xor_key_file: Option<PathBuf>,
#[arg(short, long)]
verbose: bool,
} }
const HELP: &str = r#"Keybinds: const HELP: &str = r#"Keybinds:
@ -22,16 +29,31 @@ const HELP: &str = r#"Keybinds:
Q - Quit monoclient Q - Quit monoclient
H - Show this help"#; H - Show this help"#;
macro_rules! verbose {
($($arg:tt)*) => {{
if *VERBOSE.get().unwrap() {
crossterm::execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap();
println!("{}", format_args!($($arg)*));
crossterm::execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap();
}
}};
}
fn main() { fn main() {
let args = Args::parse(); let args = Args::parse();
VERBOSE.set(args.verbose).unwrap();
std::thread::spawn(move || { std::thread::spawn(move || {
monolib::run( monolib::run(
&args.address, &args.address,
args.xor_key_file.map(|key| std::fs::read(key).expect("Failed to read preshared key")), args.xor_key_file.map(|key| std::fs::read(key).expect("Failed to read preshared key")),
Settings {
encoder: Encoder::PcmFloat,
cover: -1,
},
) )
}); });
while monolib::get_metadata().is_none() {} while monolib::get_metadata().is_none() {}
let mut md = monolib::get_metadata().unwrap(); let mut md = monolib::get_metadata().unwrap();
verbose!("md: {:?}", md);
let mut track_start = Instant::now(); let mut track_start = Instant::now();
let mut seconds_past = 0; let mut seconds_past = 0;
crossterm::execute!( crossterm::execute!(
@ -101,9 +123,10 @@ fn main() {
} }
} }
if monolib::get_metadata().unwrap() != md if monolib::get_metadata().unwrap() != md
&& track_length <= (Instant::now() - track_start).as_secs_f64() //&& track_length <= (Instant::now() - track_start).as_secs_f64()
{ {
md = next_md.clone(); md = next_md.clone();
verbose!("md: {:?}", md);
crossterm::execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap(); crossterm::execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap();
print!( print!(
"Playing: {} - {} - {} (0:00 / {}:{:02})", "Playing: {} - {} - {} (0:00 / {}:{:02})",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "monolib" name = "monolib"
version = "0.5.0" version = "0.6.0"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
description = "A library implementing the lonely radio audio streaming protocol" description = "A library implementing the lonely radio audio streaming protocol"
@ -9,11 +9,19 @@ authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
[lib] [lib]
name = "monolib" name = "monolib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["cdylib", "staticlib", "rlib"]
[dependencies] [dependencies]
rodio = { version = "0.17.3", default-features = false } rodio = { version = "0.17.3", default-features = false }
byteorder = "1.5.0" byteorder = "1.5.0"
rmp-serde = "1.1.2" rmp-serde = "1.1.2"
lonelyradio_types = { version = "0.5.0", path = "../lonelyradio_types" } lonelyradio_types = { version = "0.6.0", path = "../lonelyradio_types" }
claxon = "0.4.3" claxon = "0.4.3"
[package.metadata.xcframework]
include-dir = "src"
lib-type = "cdylib"
zip = false
macOS = true
iOS = true
simulators = true

12
monolib/cbindgen.toml Normal file
View file

@ -0,0 +1,12 @@
language = "C"
include_version = false
############################ Code Style Options ################################
braces = "SameLine"
line_length = 100
tab_width = 2
line_endings = "LF"

View file

@ -1,18 +1,46 @@
use crate::*; use crate::*;
use std::ffi::{c_char, c_float, c_ushort}; use std::ffi::{c_char, c_float};
use std::ffi::{CStr, CString}; use std::ffi::{CStr, CString};
#[repr(C)]
#[derive(Debug, PartialEq)]
pub struct CTrackMetadata {
pub title: *mut c_char,
pub album: *mut c_char,
pub artist: *mut c_char,
}
pub const ENCODER_PCM16: u8 = 0;
pub const ENCODER_PCMFLOAT: u8 = 1;
pub const ENCODER_FLAC: u8 = 2;
#[repr(C)]
#[derive(Clone, Debug, PartialEq)]
pub struct CSettings {
/// See lonelyradio_types -> Encoder
pub encoder: u8,
pub cover: i32,
}
#[no_mangle] #[no_mangle]
#[allow(clippy::not_unsafe_ptr_arg_deref)] #[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn c_start(server: *const c_char) { pub extern "C" fn c_start(server: *const c_char, settings: CSettings) {
let serv = unsafe { CStr::from_ptr(server) }; let serv = unsafe { CStr::from_ptr(server) };
run( run(
match serv.to_str() { match serv.to_str() {
Ok(s) => s, Ok(s) => s,
_ => "", _ => "",
}, },
None, Settings {
encoder: match settings.encoder {
0 => Encoder::Pcm16,
1 => Encoder::PcmFloat,
2 => Encoder::Flac,
_ => return,
},
cover: settings.cover,
},
) )
} }
@ -27,9 +55,9 @@ pub extern "C" fn c_stop() {
} }
#[no_mangle] #[no_mangle]
pub extern "C" fn c_get_state() -> c_ushort { pub extern "C" fn c_get_state() -> c_char {
let state = STATE.read().unwrap(); let state = STATE.read().unwrap();
*state as c_ushort *state as c_char
} }
#[no_mangle] #[no_mangle]
@ -69,10 +97,55 @@ pub extern "C" fn c_get_metadata_title() -> *mut c_char {
} }
#[no_mangle] #[no_mangle]
pub extern "C" fn c_get_metadata_length() -> *mut c_float { pub extern "C" fn c_get_metadata_length() -> c_float {
let md = MD.read().unwrap(); let md = MD.read().unwrap();
match md.as_ref() { match md.as_ref() {
Some(md) => &mut (md.track_length_secs as c_float + md.track_length_frac as c_float), Some(md) => md.track_length_secs as c_float + md.track_length_frac as c_float,
None => &mut 0.0, None => 0.0,
} }
} }
#[repr(C)]
pub struct CImageJpeg {
pub length: u32,
pub bytes: *mut u8,
}
/// # Safety
/// Manually deallocate returned memory after use
#[no_mangle]
pub unsafe extern "C" fn c_get_cover_jpeg() -> CImageJpeg {
let md = MD.read().unwrap();
if let Some(md) = md.as_ref() {
if let Some(cov) = md.cover.as_ref() {
//eprintln!("{} {:p}", *len, cov.as_ptr());
let len = cov.len() as u32;
//let b = Box::new(.as_slice());
let clone = cov.clone();
let ptr = clone.as_ptr() as *mut u8;
std::mem::forget(clone);
CImageJpeg {
length: len,
bytes: ptr,
}
} else {
eprintln!("No cov");
CImageJpeg {
length: 0,
bytes: std::ptr::null_mut(),
}
}
} else {
eprintln!("No md");
CImageJpeg {
length: 0,
bytes: std::ptr::null_mut(),
}
}
}
/// # Safety
/// None
#[no_mangle]
pub unsafe extern "C" fn c_drop(ptr: *mut u8, count: usize) {
std::alloc::dealloc(ptr, std::alloc::Layout::from_size_align(count, 1).unwrap());
}

View file

@ -16,20 +16,20 @@
/// Functions, providing C-like API /// Functions, providing C-like API
pub mod c; pub mod c;
mod reader;
use byteorder::{LittleEndian, ReadBytesExt}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use lonelyradio_types::{Message, TrackMetadata}; use lonelyradio_types::{Encoder, Message, ServerCapabilities, Settings, TrackMetadata};
use rodio::buffer::SamplesBuffer; use rodio::buffer::SamplesBuffer;
use rodio::{OutputStream, Sink}; use rodio::{OutputStream, Sink};
use std::error::Error; use std::error::Error;
use std::io::{BufReader, Read}; use std::io::{Read, Write};
use std::net::TcpStream; use std::net::TcpStream;
use std::sync::atomic::AtomicU8; use std::sync::atomic::AtomicU8;
use std::sync::RwLock; use std::sync::RwLock;
use std::time::Instant; use std::time::Instant;
const CACHE_SIZE: usize = 128; const CACHE_SIZE_PCM: usize = 32;
const CACHE_SIZE_COMPRESSED: usize = 2;
static SINK: RwLock<Option<Sink>> = RwLock::new(None); static SINK: RwLock<Option<Sink>> = RwLock::new(None);
static VOLUME: AtomicU8 = AtomicU8::new(255); static VOLUME: AtomicU8 = AtomicU8::new(255);
@ -76,7 +76,8 @@ pub fn stop() {
let sink = SINK.read().unwrap(); let sink = SINK.read().unwrap();
if let Some(sink) = sink.as_ref() { if let Some(sink) = sink.as_ref() {
sink.pause() sink.pause();
sink.clear()
} }
drop(sink); drop(sink);
drop(state); drop(state);
@ -143,12 +144,16 @@ pub fn set_volume(volume: u8) {
} }
/// Download track as samples /// Download track as samples
pub fn get_track(server: &str, xor_key: Option<Vec<u8>>) -> Option<(TrackMetadata, Vec<i16>)> { pub fn get_track(server: &str, mut settings: Settings) -> Option<(TrackMetadata, Vec<i16>)> {
let mut stream = BufReader::new(match xor_key { let mut connection = unwrap(TcpStream::connect(server))?;
Some(k) => reader::Reader::XorEncrypted(TcpStream::connect(server).unwrap(), k, 0), unwrap(connection.write_u64::<LittleEndian>(lonelyradio_types::HELLO_MAGIC))?;
None => reader::Reader::Unencrypted(TcpStream::connect(server).unwrap()), let capabilities: ServerCapabilities = unwrap(rmp_serde::from_read(&mut connection))?;
}); if !capabilities.encoders.contains(&settings.encoder) {
settings.encoder = Encoder::Pcm16
}
unwrap(connection.write_all(&rmp_serde::to_vec_named(&settings).unwrap()))?;
let mut stream = connection;
let mut samples = vec![]; let mut samples = vec![];
let mut md: Option<TrackMetadata> = None; let mut md: Option<TrackMetadata> = None;
loop { loop {
@ -160,47 +165,59 @@ pub fn get_track(server: &str, xor_key: Option<Vec<u8>>) -> Option<(TrackMetadat
} }
md = Some(tmd); md = Some(tmd);
} }
Message::F(fmd) => { Message::F(fmd) => match md.as_ref().unwrap().encoder {
if !md.clone().unwrap().flac { Encoder::Pcm16 => {
let mut buf = vec![0; fmd.length as usize]; let mut buf = vec![0; fmd.length as usize];
stream.read_i16_into::<LittleEndian>(&mut buf).unwrap(); stream.read_i16_into::<LittleEndian>(&mut buf).unwrap();
samples.append(&mut buf); samples.append(&mut buf);
} else { }
let take = stream.by_ref().take(fmd.length); Encoder::PcmFloat => unimplemented!(),
Encoder::Flac => {
let take = std::io::Read::by_ref(&mut stream).take(fmd.length);
let mut reader = claxon::FlacReader::new(take).unwrap(); let mut reader = claxon::FlacReader::new(take).unwrap();
samples.append( samples.append(
&mut reader.samples().map(|x| x.unwrap_or(0) as i16).collect::<Vec<i16>>(), &mut reader.samples().map(|x| x.unwrap_or(0) as i16).collect::<Vec<i16>>(),
); );
} }
} },
} }
} }
md.map(|md| (md, samples)) md.map(|md| (md, samples))
} }
fn unwrap<T, E: Error>(thing: Result<T, E>) -> T { fn unwrap<T, E: Error>(thing: Result<T, E>) -> Option<T> {
if thing.is_err() { if thing.is_err() {
*STATE.write().unwrap() = State::NotStarted; *STATE.write().unwrap() = State::NotStarted;
} }
thing.unwrap() thing.ok()
} }
/// Starts playing at "server:port" /// Starts playing at "server:port"
pub fn run(server: &str, xor_key: Option<Vec<u8>>) { pub fn run(server: &str, settings: Settings) {
let _ = _run(server, settings);
}
pub fn _run(server: &str, settings: Settings) -> Option<()> {
let mut settings = settings;
let mut state = STATE.write().unwrap(); let mut state = STATE.write().unwrap();
if *state == State::Playing || *state == State::Paused { if *state == State::Playing || *state == State::Paused {
return; return None;
} }
*state = State::Playing; *state = State::Playing;
drop(state); drop(state);
let mut stream = BufReader::new(match xor_key { let mut connection = unwrap(TcpStream::connect(server))?;
Some(k) => reader::Reader::XorEncrypted(unwrap(TcpStream::connect(server)), k, 0), unwrap(connection.write_u64::<LittleEndian>(lonelyradio_types::HELLO_MAGIC))?;
None => reader::Reader::Unencrypted(unwrap(TcpStream::connect(server))), let capabilities: ServerCapabilities = unwrap(rmp_serde::from_read(&mut connection))?;
}); if !capabilities.encoders.contains(&settings.encoder) {
settings.encoder = Encoder::Pcm16
}
unwrap(connection.write_all(&rmp_serde::to_vec_named(&settings).unwrap()))?;
let mut stream = connection;
let mut sink = SINK.write().unwrap(); let mut sink = SINK.write().unwrap();
let (_stream, stream_handle) = unwrap(OutputStream::try_default()); let (_stream, stream_handle) = unwrap(OutputStream::try_default())?;
// Can't reuse old sink for some reason // Can't reuse old sink for some reason
let audio_sink = Sink::try_new(&stream_handle).unwrap(); let audio_sink = Sink::try_new(&stream_handle).unwrap();
@ -215,7 +232,7 @@ pub fn run(server: &str, xor_key: Option<Vec<u8>>) {
// No metadata shift // No metadata shift
if watching_sleep_until_end() { if watching_sleep_until_end() {
_stop(); _stop();
return; return None;
} }
let mut md = MD.write().unwrap(); let mut md = MD.write().unwrap();
*md = Some(tmd.clone()); *md = Some(tmd.clone());
@ -227,49 +244,60 @@ pub fn run(server: &str, xor_key: Option<Vec<u8>>) {
} }
if *STATE.read().unwrap() == State::Resetting { if *STATE.read().unwrap() == State::Resetting {
_stop(); _stop();
return; return None;
} }
if !MD.read().unwrap().clone().unwrap().flac { match MD.read().unwrap().as_ref().unwrap().encoder {
let mut samples_i16 = vec![0; fmd.length as usize]; Encoder::Pcm16 => {
let mut samples_i16 = vec![0; fmd.length as usize / 2];
if stream.read_i16_into::<LittleEndian>(&mut samples_i16).is_err() { if stream.read_i16_into::<LittleEndian>(&mut samples_i16).is_err() {
return; return None;
}; };
samples.append( samples.append(
&mut samples_i16.iter().map(|sample| *sample as f32 / 32767.0).collect(), &mut samples_i16
.iter()
.map(|sample| *sample as f32 / 32767.0)
.collect(),
); );
} else { }
let take = stream.by_ref().take(fmd.length); Encoder::PcmFloat => {
let mut samples_f32 = vec![0f32; fmd.length as usize / 4];
if stream.read_f32_into::<LittleEndian>(&mut samples_f32).is_err() {
return None;
};
samples.append(&mut samples_f32);
}
Encoder::Flac => {
let take = std::io::Read::by_ref(&mut stream).take(fmd.length);
let mut reader = claxon::FlacReader::new(take).unwrap(); let mut reader = claxon::FlacReader::new(take).unwrap();
samples.append( samples.append(
&mut reader &mut reader
.samples() .samples()
.map(|x| x.unwrap_or(0) as f32 / 32767.0) .map(|x| x.unwrap_or(0) as f32 / 32768.0 / 256.0)
.collect::<Vec<f32>>(), .collect::<Vec<f32>>(),
); );
} }
};
// Sink's thread is detached from main thread, so we need to synchronize with it // Synchronizing with sink
// Why we should synchronize with it?
// Let's say, that if we don't synchronize with it, we would have
// a lot (no upper limit, actualy) of buffered sound, waiting for playing in
// sink
let sink = SINK.read().unwrap(); let sink = SINK.read().unwrap();
let _md = MD.read().unwrap(); let _md = MD.read().unwrap();
let md = _md.as_ref().unwrap().clone(); let md = _md.as_ref().unwrap().clone();
drop(_md); drop(_md);
if let Some(sink) = sink.as_ref() { if let Some(sink) = sink.as_ref() {
while sink.len() >= CACHE_SIZE { while (sink.len() >= CACHE_SIZE_PCM && md.encoder != Encoder::Flac)
|| (sink.len() >= CACHE_SIZE_COMPRESSED && md.encoder == Encoder::Flac)
{
// Sleeping exactly one buffer and watching for reset signal // Sleeping exactly one buffer and watching for reset signal
if watching_sleep( if watching_sleep(
if sink.len() > 2 { if sink.len() > 2 {
sink.len() as f32 - 2.0 sink.len() as f32 - 2.0
} else { } else {
0.25 0.25
} * fmd.length as f32 / md.sample_rate as f32 } * samples.len() as f32 / md.sample_rate as f32
/ 4.0, / 4.0,
) { ) {
_stop(); _stop();
return; return None;
} }
} }
sink.append(SamplesBuffer::new( sink.append(SamplesBuffer::new(

View file

@ -0,0 +1,5 @@
framework module MonoLib {
// a header file in the same directory as the modulemap
header "monolib.h"
export *
}

View file

@ -1,20 +1,42 @@
#include <stdarg.h> #include <stdarg.h>
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
void c_start(const char *server); typedef struct CImageJpeg {
uint32_t length;
uint8_t *bytes;
} CImageJpeg;
void c_toggle(void); typedef struct CSettings {
/**
* See lonelyradio_types -> Encoder
*/
uint8_t encoder;
int32_t cover;
} CSettings;
void c_stop(void); void c_drop(uint8_t *ptr, size_t count);
unsigned short c_get_state(void); /**
* # Safety
char *c_get_metadata_artist(void); * Manually deallocate returned memory after use
*/
struct CImageJpeg c_get_cover_jpeg(void);
char *c_get_metadata_album(void); char *c_get_metadata_album(void);
char *c_get_metadata_artist(void);
float c_get_metadata_length(void);
char *c_get_metadata_title(void); char *c_get_metadata_title(void);
float *c_get_metadata_length(void); char c_get_state(void);
void c_start(const char *server, struct CSettings settings);
void c_stop(void);
void c_toggle(void);

View file

@ -1,25 +0,0 @@
use std::{io, net::TcpStream};
pub(crate) enum Reader {
Unencrypted(TcpStream),
XorEncrypted(TcpStream, Vec<u8>, u64),
}
impl io::Read for Reader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Self::Unencrypted(s) => s.read(buf),
Self::XorEncrypted(s, key, n) => {
let out = s.read(buf);
if let Ok(i) = &out {
for k in buf.iter_mut().take(*i) {
*k ^= key[*n as usize];
*n += 1;
*n %= key.len() as u64;
}
}
out
}
}
}
}

1
monolib/src/target Symbolic link
View file

@ -0,0 +1 @@
../target

1
monolib/target Symbolic link
View file

@ -0,0 +1 @@
../target

View file

@ -1,6 +1,6 @@
[package] [package]
name = "monoloader" name = "monoloader"
version = "0.4.0" version = "0.6.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -9,3 +9,4 @@ edition = "2021"
monolib = { path = "../monolib" } monolib = { path = "../monolib" }
clap = { version = "4.4.18", features = ["derive"] } clap = { version = "4.4.18", features = ["derive"] }
hound = "3.5.1" hound = "3.5.1"
lonelyradio_types = { version = "0.6.0", path = "../lonelyradio_types" }

View file

@ -1,4 +1,5 @@
use clap::Parser; use clap::Parser;
use lonelyradio_types::Settings;
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Parser)] #[derive(Parser)]
@ -14,7 +15,10 @@ fn main() {
let args = Args::parse(); let args = Args::parse();
let (md, samples) = monolib::get_track( let (md, samples) = monolib::get_track(
&args.address, &args.address,
args.xor_key_file.map(|key| std::fs::read(key).expect("Failed to read preshared key")), Settings {
encoder: lonelyradio_types::Encoder::Pcm16,
cover: -1,
},
) )
.unwrap(); .unwrap();
println!( println!(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

View file

@ -1,13 +0,0 @@
//
// MonoLib-Bridging-Header.h
// monoclient
//
// Created by ivabus on 03.03.2024.
//
#ifndef MonoLib_Bridging_Header_h
#define MonoLib_Bridging_Header_h
#import "monolib.h"
#endif /* MonoLib_Bridging_Header_h */

View file

@ -1,21 +0,0 @@
# Platform-specific player realizations
## Rust + SwiftUI (iOS/iPadOS/macOS (iOS mode))
### Build `monolib`
```
cargo lipo --release --targets aarch64-apple-ios -p monolib
```
For running in simulator
```
cargo lipo --release --targets aarch64-apple-ios-sim,x86_64-apple-ios -p monolib
```
### Build and run app
Open Xcode and run.
[Screenshots (pre v0.2)](./screenshots/swiftui)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View file

@ -1,116 +0,0 @@
//
// ContentView.swift
// monoclient
//
// Created by ivabus on 03.03.2024.
//
import AVFAudio
import SwiftUI
class MonoLib {
func run(server: String) async {
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(
.playback, mode: .default,
policy: .longFormAudio)
try audioSession.setActive(true)
} catch {
print("Failed to set the audio session configuration")
}
c_start(server)
}
}
struct ContentView: View {
let timer = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()
@State private var server: String = ""
@State private var port: String = ""
@State private var playing: Bool = true
@State private var running: Bool = false
@State var now_playing_artist: String = ""
@State var now_playing_album: String = ""
@State var now_playing_title: String = ""
var body: some View {
VStack {
Text("Monoclient").font(.largeTitle).fontWidth(.expanded).bold()
VStack(alignment: .center) {
HStack {
Text("Server").frame(minWidth: 50, idealWidth: 60)
TextField(
"Required",
text: $server
)
.disableAutocorrection(true)
}
.textFieldStyle(.roundedBorder)
HStack {
Text("Port").frame(minWidth: 50, idealWidth: 60)
TextField(
"Required",
text: $port
)
.disableAutocorrection(true).keyboardType(.numberPad).keyboardShortcut(.escape)
}
.textFieldStyle(.roundedBorder)
Button(action: {
if running {
playing = !playing
c_toggle()
}
running = true
let a = MonoLib()
Task.init {
await a.run(server: server + ":" + port)
}
}) {
Image(
systemName: running
? (playing ? "pause.circle.fill" : "play.circle") : "infinity.circle"
).font(.largeTitle)
}.buttonStyle(
.borderedProminent)
HStack{
Button(action: {
c_stop()
running = false
playing = true
}) { Image(systemName: "stop").font(.title3) }.buttonStyle(
.bordered
).disabled(!running)
Button(action: {
c_stop()
playing = true
let a = MonoLib()
Task.init {
await a.run(server: server + ":" + port)
}
}) {Image(systemName: "forward").font(.title3)}.buttonStyle(.bordered).disabled(!running)
}
}.frame(width: 300)
VStack(spacing: 10) {
Text(now_playing_artist).onReceive(timer) { _ in
now_playing_artist = String(cString: c_get_metadata_artist()!)
}
Text(now_playing_album).onReceive(timer) { _ in
now_playing_album = String(cString: c_get_metadata_album()!)
}
Text(now_playing_title).onReceive(timer) { _ in
now_playing_title = String(cString: c_get_metadata_title()!)
}.bold()
}.frame(minHeight: 100)
}.padding()
}
}
#Preview {
ContentView()
}

View file

@ -1,17 +0,0 @@
//
// monoclientApp.swift
// monoclient
//
// Created by ivabus on 03.03.2024.
//
import SwiftUI
@main
struct monoclientApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

View file

@ -65,19 +65,11 @@ pub async fn get_meta(file_path: &Path) -> (u16, u32, Time) {
} }
let args = Args::parse(); let args = Args::parse();
( (channels, get_resampling_rate(&sample_rate, &args.max_samplerate), track_length)
channels,
if sample_rate > args.max_samplerate {
args.max_samplerate
} else {
sample_rate
},
track_length,
)
} }
/// Getting samples /// Getting samples
pub fn decode_file_stream(file_path: PathBuf) -> impl Stream<Item = Vec<i16>> { pub fn decode_file_stream(file_path: PathBuf) -> impl Stream<Item = Vec<f32>> {
let args = Args::parse(); let args = Args::parse();
let file = Box::new(std::fs::File::open(&file_path).unwrap()); let file = Box::new(std::fs::File::open(&file_path).unwrap());
let mut hint = Hint::new(); let mut hint = Hint::new();
@ -128,7 +120,7 @@ pub fn decode_file_stream(file_path: PathBuf) -> impl Stream<Item = Vec<i16>> {
// We are downsampling, not upsampling, so we should be fine // We are downsampling, not upsampling, so we should be fine
yield ( yield (
if output_rate == spec.rate { if output_rate == spec.rate {
byte_buf.samples().iter().map(|x| (*x * 32768.0) as i16).collect() byte_buf.samples().to_vec()
} else { } else {
samplerate::convert( samplerate::convert(
spec.rate, spec.rate,
@ -138,17 +130,14 @@ pub fn decode_file_stream(file_path: PathBuf) -> impl Stream<Item = Vec<i16>> {
byte_buf.samples(), byte_buf.samples(),
) )
.unwrap() .unwrap()
.iter()
.map(|x| (*x * 32768.0) as i16)
.collect()
} }
); );
} else { } else {
let mut byte_buf = let mut byte_buf =
SampleBuffer::<i16>::new(decoded.capacity() as u64, *decoded.spec()); SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec());
byte_buf.copy_interleaved_ref(decoded); byte_buf.copy_interleaved_ref(decoded);
yield (byte_buf.samples().to_vec()); yield byte_buf.samples().to_vec();
} }
continue; continue;
} }

52
src/encode.rs Normal file
View file

@ -0,0 +1,52 @@
use flacenc::{component::BitRepr, error::Verify, source::MemSource};
use lonelyradio_types::Encoder;
pub fn encode(
codec: Encoder,
mut samples: Vec<f32>,
sample_rate: u32,
channels: u16,
) -> Option<Vec<u8>> {
match codec {
Encoder::Pcm16 => {
let mut samples = samples.iter_mut().map(|x| (*x * 32768.0) as i16).collect::<Vec<_>>();
// Launching lonelyradio on the router moment
if cfg!(target_endian = "big") {
samples.iter_mut().for_each(|sample| {
*sample = sample.to_le();
});
}
// Sowwy about that
let (_, samples, _) = unsafe { samples.align_to::<u8>() };
Some(samples.to_vec())
}
Encoder::PcmFloat => {
// Launching lonelyradio on the router moment
// Sowwy about that
let samples = samples.iter().map(|x| x.to_bits()).collect::<Vec<u32>>();
let (_, samples, _) = unsafe { samples.align_to::<u8>() };
Some(samples.to_vec())
}
Encoder::Flac => {
let encoded = flacenc::encode_with_fixed_block_size(
&flacenc::config::Encoder::default().into_verified().unwrap(),
MemSource::from_samples(
// I'm crying (It's just a burning memory)
&samples
.iter()
.map(|x| (*x as f64 * 32768.0 * 256.0) as i32)
.collect::<Vec<i32>>(),
channels as usize,
24,
sample_rate as usize,
),
256,
)
.unwrap();
let mut sink = flacenc::bitsink::ByteSink::new();
encoded.write(&mut sink).unwrap();
Some(sink.as_slice().to_vec())
}
}
}

View file

@ -1,27 +1,30 @@
mod decode; mod decode;
mod writer; mod encode;
use std::io::Cursor;
use std::io::Read;
use std::net::TcpStream;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use chrono::Local; use chrono::Local;
use clap::Parser; use clap::Parser;
use flacenc::component::BitRepr; use encode::encode;
use flacenc::error::Verify;
use flacenc::source::MemSource;
use futures_util::pin_mut; use futures_util::pin_mut;
use futures_util::StreamExt; use futures_util::StreamExt;
use image::io::Reader as ImageReader;
use lofty::Accessor; use lofty::Accessor;
use lofty::TaggedFileExt; use lofty::TaggedFileExt;
use lonelyradio_types::Encoder;
use lonelyradio_types::ServerCapabilities;
use lonelyradio_types::Settings;
use lonelyradio_types::{FragmentMetadata, Message, TrackMetadata}; use lonelyradio_types::{FragmentMetadata, Message, TrackMetadata};
use once_cell::sync::Lazy;
use rand::prelude::*; use rand::prelude::*;
use std::io::Write; use std::io::Write;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio_stream::Stream; use tokio_stream::Stream;
use walkdir::DirEntry; use walkdir::DirEntry;
use writer::Writer;
use crate::decode::decode_file_stream; use crate::decode::decode_file_stream;
use crate::decode::get_meta; use crate::decode::get_meta;
@ -51,48 +54,37 @@ struct Args {
#[arg(long)] #[arg(long)]
no_resampling: bool, no_resampling: bool,
/// Use FLAC compression /// Size of artwork (-1 for no artwork, 0 for original, N for NxN)
#[arg(short, long)] #[arg(long, default_value = "96000")]
flac: bool, artwork: i32,
/// Enable XOR "encryption"
#[arg(long)]
xor_key_file: Option<PathBuf>,
} }
static KEY: Lazy<Option<Arc<Vec<u8>>>> = Lazy::new(|| { const SUPPORTED_ENCODERS: [Encoder; 3] = [Encoder::Pcm16, Encoder::PcmFloat, Encoder::Flac];
let args = Args::parse();
if let Some(path) = args.xor_key_file {
let key = std::fs::read(path).expect("Failed to read preshared key");
Some(Arc::new(key))
} else {
None
}
});
async fn stream_track( async fn stream_track(
samples_stream: impl Stream<Item = Vec<i16>>, samples_stream: impl Stream<Item = Vec<f32>>,
war: bool, war: bool,
md: TrackMetadata, md: TrackMetadata,
s: &mut Writer, s: &mut TcpStream,
) -> bool { ) -> bool {
pin_mut!(samples_stream); pin_mut!(samples_stream);
let _md = md.clone(); let _md = md.clone();
if s.write_all(rmp_serde::to_vec(&Message::T(_md)).unwrap().as_slice()).is_err() { if s.write_all(rmp_serde::encode::to_vec_named(&Message::T(_md)).unwrap().as_slice()).is_err() {
return true; return true;
}; };
// Why chunks? // Why chunks?
// flacenc is broken on low amount of samples (Symphonia's AIFF decoder returns ~2304 // flacenc is broken on low amount of samples (Symphonia's AIFF decoder returns
// samples per packet (on bo en's tracks), instead of usual ~8192 on any other lossless decoder) // ~2304 samples per packet (on bo en's tracks), instead of usual ~8192 on any
// other lossless decoder)
while let Some(mut _samples) = samples_stream while let Some(mut _samples) = samples_stream
.as_mut() .as_mut()
.chunks(if md.flac && md.track_length_secs > 1 { .chunks(match md.encoder {
2 Encoder::Pcm16 => 1,
} else { Encoder::PcmFloat => 1,
1 Encoder::Flac => 16,
}) })
.next() .next()
.await .await
@ -100,59 +92,53 @@ async fn stream_track(
let mut _samples = _samples.concat(); let mut _samples = _samples.concat();
if war { if war {
_samples.iter_mut().for_each(|sample| { _samples.iter_mut().for_each(|sample| {
*sample = sample.signum() * 32767; *sample = sample.signum();
}); });
} }
if !md.flac { match md.encoder {
Encoder::Pcm16 => {
let _md = Message::F(FragmentMetadata { let _md = Message::F(FragmentMetadata {
length: _samples.len() as u64, length: _samples.len() as u64 * 2,
}); });
if s.write_all(rmp_serde::to_vec(&_md).unwrap().as_slice()).is_err() { if s.write_all(rmp_serde::to_vec(&_md).unwrap().as_slice()).is_err() {
return true; return true;
} }
if s.write_all(
// Launching lonelyradio on the router moment &encode(Encoder::Pcm16, _samples, md.sample_rate, md.channels).unwrap(),
if cfg!(target_endian = "big") { )
_samples.iter_mut().for_each(|sample| { .is_err()
*sample = sample.to_le(); {
});
}
// Sowwy about that
let (_, samples, _) = unsafe { _samples.align_to::<u8>() };
if s.write_all(samples).is_err() {
return true; return true;
} }
} else {
let encoded = flacenc::encode_with_fixed_block_size(
&flacenc::config::Encoder::default().into_verified().unwrap(),
MemSource::from_samples(
// I'm crying (It's just a burning memory)
&_samples.iter().map(|x| *x as i32).collect::<Vec<i32>>(),
md.channels as usize,
16,
md.sample_rate as usize,
),
256,
);
if encoded.is_err() {
return true;
} }
Encoder::PcmFloat => {
let mut sink = flacenc::bitsink::ByteSink::new();
encoded.unwrap().write(&mut sink).unwrap();
let _md = Message::F(FragmentMetadata { let _md = Message::F(FragmentMetadata {
length: sink.as_slice().len() as u64, length: _samples.len() as u64 * 4,
}); });
if s.write_all(rmp_serde::to_vec(&_md).unwrap().as_slice()).is_err() { if s.write_all(rmp_serde::to_vec(&_md).unwrap().as_slice()).is_err() {
return true; return true;
} }
if s.write_all(sink.as_slice()).is_err() { if s.write_all(
&encode(Encoder::PcmFloat, _samples, md.sample_rate, md.channels).unwrap(),
)
.is_err()
{
return true; return true;
} }
} }
Encoder::Flac => {
let encoded = encode(Encoder::Flac, _samples, md.sample_rate, md.channels).unwrap();
let _md = Message::F(FragmentMetadata {
length: encoded.as_slice().len() as u64,
});
if s.write_all(rmp_serde::to_vec(&_md).unwrap().as_slice()).is_err() {
return true;
}
if s.write_all(encoded.as_slice()).is_err() {
return true;
}
}
}
} }
false false
} }
@ -160,9 +146,9 @@ async fn stream_track(
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let args = Args::parse(); let args = Args::parse();
let listener = TcpListener::bind(Args::parse().address).await.unwrap(); let listener = TcpListener::bind(args.address).await.unwrap();
let tracklist = Arc::new( let tracklist = Arc::new(
walkdir::WalkDir::new(Args::parse().dir) walkdir::WalkDir::new(args.dir)
.into_iter() .into_iter()
.filter_entry(is_not_hidden) .filter_entry(is_not_hidden)
.filter_map(|v| v.ok()) .filter_map(|v| v.ok())
@ -172,25 +158,36 @@ async fn main() {
); );
loop { loop {
let (socket, _) = listener.accept().await.unwrap(); let (socket, _) = listener.accept().await.unwrap();
let s = socket.into_std().unwrap(); let mut s = socket.into_std().unwrap();
s.set_nonblocking(false).unwrap(); s.set_nonblocking(false).unwrap();
let s = if args.xor_key_file.is_some() { let mut hello = [0u8; 8];
Writer::XorEncrypted( if s.read_exact(&mut hello).is_err() {
s, continue;
match &*KEY {
Some(a) => a.clone(),
_ => {
unreachable!()
} }
}, if hello != lonelyradio_types::HELLO_MAGIC.to_le_bytes() {
0, continue;
}
if s.write_all(
&rmp_serde::to_vec_named(&ServerCapabilities {
encoders: SUPPORTED_ENCODERS.to_vec(),
})
.unwrap(),
) )
} else { .is_err()
Writer::Unencrypted(s) {
continue;
}; };
tokio::spawn(stream(s, tracklist.clone())); let settings: Settings = match rmp_serde::from_read(&s) {
Ok(s) => s,
_ => continue,
};
if settings.cover < -1 {
continue;
}
tokio::spawn(stream(s, tracklist.clone(), settings));
} }
} }
fn is_not_hidden(entry: &DirEntry) -> bool { fn is_not_hidden(entry: &DirEntry) -> bool {
entry.file_name().to_str().map(|s| entry.depth() == 0 || !s.starts_with('.')).unwrap_or(false) entry.file_name().to_str().map(|s| entry.depth() == 0 || !s.starts_with('.')).unwrap_or(false)
} }
@ -207,7 +204,7 @@ fn track_valid(track: &Path) -> bool {
true true
} }
async fn stream(mut s: Writer, tracklist: Arc<Vec<PathBuf>>) { async fn stream(mut s: TcpStream, tracklist: Arc<Vec<PathBuf>>, settings: Settings) {
let args = Args::parse(); let args = Args::parse();
loop { loop {
@ -216,6 +213,7 @@ async fn stream(mut s: Writer, tracklist: Arc<Vec<PathBuf>>) {
let mut title = String::new(); let mut title = String::new();
let mut artist = String::new(); let mut artist = String::new();
let mut album = String::new(); let mut album = String::new();
let mut cover = std::thread::spawn(|| None);
let mut file = std::fs::File::open(&track).unwrap(); let mut file = std::fs::File::open(&track).unwrap();
let tagged = match lofty::read_from(&mut file) { let tagged = match lofty::read_from(&mut file) {
Ok(f) => f, Ok(f) => f,
@ -224,12 +222,33 @@ async fn stream(mut s: Writer, tracklist: Arc<Vec<PathBuf>>) {
if let Some(id3v2) = tagged.primary_tag() { if let Some(id3v2) = tagged.primary_tag() {
title = title =
id3v2.title().unwrap_or(track.file_stem().unwrap().to_string_lossy()).to_string(); id3v2.title().unwrap_or(track.file_stem().unwrap().to_string_lossy()).to_string();
album = id3v2.album().unwrap_or("[No tag]".into()).to_string(); album = id3v2.album().unwrap_or("".into()).to_string();
artist = id3v2.artist().unwrap_or("[No tag]".into()).to_string(); artist = id3v2.artist().unwrap_or("".into()).to_string();
if !(id3v2.pictures().is_empty() || args.artwork == -1 || settings.cover == -1) {
let pic = id3v2.pictures()[0].clone();
cover = std::thread::spawn(move || {
let dec = ImageReader::new(Cursor::new(pic.into_data()))
.with_guessed_format()
.ok()?
.decode()
.ok()?;
let mut img = Vec::new();
if args.artwork != 0 && settings.cover != 0 {
let size = std::cmp::min(args.artwork as u32, settings.cover as u32);
dec.resize(size, size, image::imageops::FilterType::Lanczos3)
} else {
dec
}
.to_rgb8()
.write_to(&mut Cursor::new(&mut img), image::ImageFormat::Jpeg)
.unwrap();
Some(img)
});
};
}; };
let track_message = format!("{} - {} - {}", &artist, &album, &title); let track_message = format!("{} - {} - {}", &artist, &album, &title);
eprintln!( eprintln!(
"[{}] {} to {}:{}{}", "[{}] {} to {}:{}{} ({:?})",
Local::now().to_rfc3339(), Local::now().to_rfc3339(),
track_message, track_message,
s.peer_addr().unwrap().ip(), s.peer_addr().unwrap().ip(),
@ -238,7 +257,8 @@ async fn stream(mut s: Writer, tracklist: Arc<Vec<PathBuf>>) {
" with WAR.rs" " with WAR.rs"
} else { } else {
"" ""
} },
settings.encoder
); );
if args.public_log { if args.public_log {
@ -262,7 +282,8 @@ async fn stream(mut s: Writer, tracklist: Arc<Vec<PathBuf>>) {
TrackMetadata { TrackMetadata {
track_length_frac: time.frac as f32, track_length_frac: time.frac as f32,
track_length_secs: time.seconds, track_length_secs: time.seconds,
flac: args.flac, encoder: settings.encoder,
cover: cover.join().unwrap(),
album, album,
artist, artist,
title, title,

View file

@ -1,59 +0,0 @@
use std::{
borrow::BorrowMut,
io,
net::{SocketAddr, TcpStream},
sync::Arc,
};
pub(crate) enum Writer {
Unencrypted(TcpStream),
XorEncrypted(TcpStream, Arc<Vec<u8>>, u64),
}
impl io::Write for Writer {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Self::Unencrypted(s) => s.write(buf),
Self::XorEncrypted(s, key, n) => {
for mut k in buf.iter().copied() {
k ^= key[*n as usize];
*n += 1;
*n %= key.len() as u64;
s.write_all(&[k])?;
}
Ok(buf.len())
}
}
}
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
match self {
Self::Unencrypted(s) => s.write_all(buf),
Self::XorEncrypted(s, key, n) => s.write_all(
&buf.iter()
.borrow_mut()
.copied()
.map(|mut k| {
k ^= key[*n as usize];
*n += 1;
*n %= key.len() as u64;
k
})
// I don't like it
.collect::<Vec<u8>>(),
),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::XorEncrypted(s, _, _) | Self::Unencrypted(s) => s.flush(),
}
}
}
impl Writer {
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
match self {
Self::XorEncrypted(s, _, _) | Self::Unencrypted(s) => s.peer_addr(),
}
}
}