0.6.0: artwork support, new SwiftUI client, new protocol iteration
Drop of the "XOR encryption" Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
1125
Cargo.lock
generated
|
@ -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
|
||||||
|
|
78
README.md
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
|
||||||
|
|
BIN
monoclient-s/lonelyradio.icns
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
monoclient-s/lonelyradio.png
Normal file
After Width: | Height: | Size: 94 KiB |
227
monoclient-s/src/app.rs
Normal 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
|
@ -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();
|
||||||
|
}
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
453
monoclient-x/monoclient-x.xcodeproj/project.pbxproj
Normal 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 */;
|
||||||
|
}
|
7
monoclient-x/monoclient-x.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
|
@ -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>
|
|
@ -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>
|
|
@ -2,8 +2,8 @@
|
||||||
"colors" : [
|
"colors" : [
|
||||||
{
|
{
|
||||||
"color" : {
|
"color" : {
|
||||||
"platform" : "ios",
|
"platform" : "universal",
|
||||||
"reference" : "systemPinkColor"
|
"reference" : "systemPurpleColor"
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
|
@ -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"
|
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 72 KiB |
After Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 928 B |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 93 KiB |
After Width: | Height: | Size: 93 KiB |
After Width: | Height: | Size: 93 KiB |
71
monoclient-x/monoclient-x/ContentView.swift
Normal 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()
|
||||||
|
}
|
57
monoclient-x/monoclient-x/Metadata.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
271
monoclient-x/monoclient-x/Player.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
10
monoclient-x/monoclient-x/monoclient_x.entitlements
Normal 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>
|
35
monoclient-x/monoclient-x/monoclient_xApp.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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" }
|
||||||
|
|
||||||
|
|
|
@ -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})",
|
||||||
|
|
|
@ -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
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
language = "C"
|
||||||
|
|
||||||
|
|
||||||
|
include_version = false
|
||||||
|
|
||||||
|
############################ Code Style Options ################################
|
||||||
|
|
||||||
|
braces = "SameLine"
|
||||||
|
line_length = 100
|
||||||
|
tab_width = 2
|
||||||
|
line_endings = "LF"
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
5
monolib/src/module.modulemap
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
framework module MonoLib {
|
||||||
|
// a header file in the same directory as the modulemap
|
||||||
|
header "monolib.h"
|
||||||
|
export *
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
@ -0,0 +1 @@
|
||||||
|
../target
|
1
monolib/target
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../target
|
|
@ -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" }
|
||||||
|
|
|
@ -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!(
|
||||||
|
|
Before Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 294 KiB |
Before Width: | Height: | Size: 277 KiB |
|
@ -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 */
|
|
|
@ -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)
|
|
Before Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 673 B |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 51 KiB |
|
@ -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()
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
||||||
|
@ -122,13 +114,13 @@ pub fn decode_file_stream(file_path: PathBuf) -> impl Stream<Item = Vec<i16>> {
|
||||||
let mut byte_buf =
|
let mut byte_buf =
|
||||||
SampleBuffer::<f32>::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);
|
||||||
let output_rate = get_resampling_rate(&spec.rate, &args.max_samplerate);
|
let output_rate = get_resampling_rate(&spec.rate,&args.max_samplerate);
|
||||||
|
|
||||||
// About Samplerate struct:
|
// About Samplerate struct:
|
||||||
// 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
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
195
src/main.rs
|
@ -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,
|
||||||
|
|
|
@ -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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|