Skip to content

Commit 1446e48

Browse files
CopilotLi2CO3ICU
andauthored
feat: finalize thread-safe FFI and client-local fabric scaffolding
Agent-Logs-Url: https://github.com/Li2CO3ICU/LiPlayerPro/sessions/964e31e0-7332-44d8-ab86-5426c2791402 Co-authored-by: Li2CO3ICU <93815785+Li2CO3ICU@users.noreply.github.com>
1 parent 776827c commit 1446e48

7 files changed

Lines changed: 199 additions & 90 deletions

File tree

Cargo.toml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,18 @@ license = "AGPL-3.0-only"
1616
name = "liplayerpro_core"
1717
crate-type = ["rlib", "cdylib"]
1818

19+
[features]
20+
default = ["tui-app", "native-alsa"]
21+
tui-app = ["dep:souvlaki", "native-alsa"]
22+
native-alsa = ["dep:alsa"]
23+
24+
[[bin]]
25+
name = "LiPlayerPro"
26+
path = "src/main.rs"
27+
required-features = ["tui-app"]
28+
1929
[dependencies]
20-
alsa = "0.7"
30+
alsa = { version = "0.7", optional = true }
2131
ringbuf = "0.3"
2232
crossbeam-channel = "0.5"
2333
crossterm = "0.27"
@@ -30,6 +40,6 @@ rayon = "1.8"
3040
unicode-width = "0.1"
3141
ratatui = "0.26"
3242
fuzzy-matcher = "0.3"
33-
souvlaki = "0.7"
43+
souvlaki = { version = "0.7", optional = true }
3444
shellexpand = "3.1.0"
3545
notify = "6.1.1"

fabric-client-local/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010

1111
## 对接步骤
1212

13-
1. 在 Fabric 客户端初始化时加载 native 动态库(`liplayerpro_core`)。
13+
1. 在 Fabric 客户端初始化时加载 native 动态库:
14+
- Linux: `libliplayerpro_core.so`
15+
- Windows: `liplayerpro_core.dll`
16+
- macOS: `libliplayerpro_core.dylib`
1417
2. 创建播放器句柄并调用 `liplayer_scan_local_library` 扫描本地音乐目录。
1518
3. 通过命令/按键/HUD 操作调用:
1619
- `liplayer_play_track_at`

src/audio_engine.rs

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,20 @@
88
// -----------------------------------------------------------------------
99

1010
#![allow(dead_code, unused_variables)]
11+
#[cfg(feature = "native-alsa")]
12+
use alsa::pcm::{Access, Format, HwParams, PCM};
13+
#[cfg(feature = "native-alsa")]
1114
use alsa::{Direction, ValueOr};
1215
use crossbeam_channel::{unbounded, Receiver, Sender};
1316
use ringbuf::HeapRb;
1417
use std::io::Read;
1518
use std::process::{Command, Stdio};
16-
use std::sync::{Arc, atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}};
19+
use std::sync::{
20+
atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering},
21+
Arc,
22+
};
1723
use std::thread;
1824
use std::time::Duration;
19-
use alsa::pcm::{PCM, Format, Access, HwParams};
2025

2126
pub enum AudioCmd {
2227
Play(String),
@@ -90,11 +95,11 @@ impl AudioEngine {
9095
pub fn get_elapsed_duration(&self) -> Duration {
9196
let frames = self.current_pos.load(Ordering::Acquire);
9297
let rate = self.current_sample_rate.load(Ordering::Acquire);
93-
98+
9499
if rate == 0 {
95100
return Duration::ZERO;
96101
}
97-
102+
98103
// 🌟 提高精度:先乘后除,防止因为整数除法丢失不足 1 秒的部分
99104
Duration::from_millis((frames * 1000) / rate as u64)
100105
}
@@ -131,7 +136,7 @@ impl AudioEngine {
131136

132137
let pos_tracker_alsa = pos_tracker.clone();
133138
let pause_alsa = is_paused.clone();
134-
139+
135140
let rb = HeapRb::<i32>::new(2_097_152);
136141
let (mut prod, mut cons) = rb.split();
137142

@@ -200,7 +205,8 @@ impl AudioEngine {
200205
})
201206
.unwrap();
202207

203-
let alsa_thread = thread::Builder::new()
208+
#[cfg(feature = "native-alsa")]
209+
let output_thread = thread::Builder::new()
204210
.name("ALSA_Driver".into())
205211
.spawn(move || {
206212
while cons.len() < 80_000
@@ -227,24 +233,24 @@ impl AudioEngine {
227233
let mut local_buf = vec![0i32; 16384];
228234

229235
while flag_alsa.load(Ordering::Acquire) {
230-
231-
// 🌟 核心休眠逻辑:优雅释放声卡并挂起
232236
if pause_alsa.load(Ordering::Acquire) {
233-
let _ = pcm.drop(); // 安全丢弃缓存防止爆音
234-
while pause_alsa.load(Ordering::Acquire) && flag_alsa.load(Ordering::Acquire) {
237+
let _ = pcm.drop();
238+
while pause_alsa.load(Ordering::Acquire)
239+
&& flag_alsa.load(Ordering::Acquire)
240+
{
235241
thread::sleep(Duration::from_millis(50));
236242
}
237-
let _ = pcm.prepare(); // 取消暂停时重新激活声卡
243+
let _ = pcm.prepare();
238244
continue;
239245
}
240-
246+
241247
let avail = cons.len();
242248
if avail >= 2 {
243249
let read_len = std::cmp::min(avail, local_buf.len());
244250
let read_len = read_len - (read_len % 2);
245251
let popped = cons.pop_slice(&mut local_buf[..read_len]);
246252
if popped > 0 {
247-
if let Err(_) = io.writei(&local_buf[..popped]) {
253+
if io.writei(&local_buf[..popped]).is_err() {
248254
let _ = pcm.prepare();
249255
} else {
250256
pos_tracker_alsa.fetch_add(
@@ -264,8 +270,36 @@ impl AudioEngine {
264270
})
265271
.unwrap();
266272

273+
#[cfg(not(feature = "native-alsa"))]
274+
let output_thread = thread::Builder::new()
275+
.name("NoOp_Output".into())
276+
.spawn(move || {
277+
let mut local_buf = vec![0i32; 16384];
278+
while flag_alsa.load(Ordering::Acquire) {
279+
if pause_alsa.load(Ordering::Acquire) {
280+
thread::sleep(Duration::from_millis(50));
281+
continue;
282+
}
283+
let avail = cons.len();
284+
if avail >= 2 {
285+
let read_len = std::cmp::min(avail, local_buf.len());
286+
let read_len = read_len - (read_len % 2);
287+
let popped = cons.pop_slice(&mut local_buf[..read_len]);
288+
if popped > 0 {
289+
pos_tracker_alsa
290+
.fetch_add((popped / 2) as u64, Ordering::Relaxed);
291+
}
292+
} else if eof_alsa.load(Ordering::Acquire) {
293+
break;
294+
} else {
295+
thread::sleep(Duration::from_micros(200));
296+
}
297+
}
298+
})
299+
.unwrap();
300+
267301
pipeline_threads.push(dec_thread);
268-
pipeline_threads.push(alsa_thread);
302+
pipeline_threads.push(output_thread);
269303
}
270304
Ok(AudioCmd::Stop) => {
271305
play_flag.store(false, Ordering::Release);
@@ -309,6 +343,7 @@ impl AudioEngine {
309343
}
310344
}
311345

346+
#[cfg(feature = "native-alsa")]
312347
fn negotiate_alsa_rate(device_name: &str, target: u32) -> u32 {
313348
if let Ok(pcm) = PCM::new(device_name, Direction::Playback, false) {
314349
if let Ok(hwp) = HwParams::any(&pcm) {
@@ -319,4 +354,9 @@ impl AudioEngine {
319354
}
320355
44100
321356
}
357+
358+
#[cfg(not(feature = "native-alsa"))]
359+
fn negotiate_alsa_rate(_device_name: &str, target: u32) -> u32 {
360+
target
361+
}
322362
}

src/ffi.rs

Lines changed: 63 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use std::ffi::{CStr, CString};
22
use std::os::raw::{c_char, c_int, c_ulonglong};
33
use std::ptr::null_mut;
4+
use std::sync::Mutex;
45

56
use crate::player_core::{PlayerCore, PlayerState};
7+
type PlayerHandle = Mutex<PlayerCore>;
68

79
const OK: c_int = 0;
810
const ERR_NULL: c_int = -1;
@@ -23,7 +25,7 @@ pub extern "C" fn liplayer_create(
2325
device_name: *const c_char,
2426
music_dir: *const c_char,
2527
index_path: *const c_char,
26-
) -> *mut PlayerCore {
28+
) -> *mut PlayerHandle {
2729
let device_name = match cstr_to_string(device_name) {
2830
Ok(v) => v,
2931
Err(_) => return null_mut(),
@@ -36,15 +38,15 @@ pub extern "C" fn liplayer_create(
3638
Ok(v) => v,
3739
Err(_) => return null_mut(),
3840
};
39-
Box::into_raw(Box::new(PlayerCore::new(
41+
Box::into_raw(Box::new(Mutex::new(PlayerCore::new(
4042
&device_name,
4143
&music_dir,
4244
&index_path,
43-
)))
45+
))))
4446
}
4547

4648
#[unsafe(no_mangle)]
47-
pub extern "C" fn liplayer_destroy(handle: *mut PlayerCore) {
49+
pub extern "C" fn liplayer_destroy(handle: *mut PlayerHandle) {
4850
if handle.is_null() {
4951
return;
5052
}
@@ -56,7 +58,7 @@ pub extern "C" fn liplayer_destroy(handle: *mut PlayerCore) {
5658

5759
#[unsafe(no_mangle)]
5860
pub extern "C" fn liplayer_scan_local_library(
59-
handle: *mut PlayerCore,
61+
handle: *mut PlayerHandle,
6062
music_dir: *const c_char,
6163
) -> c_int {
6264
if handle.is_null() {
@@ -67,30 +69,37 @@ pub extern "C" fn liplayer_scan_local_library(
6769
Err(code) => return code,
6870
};
6971
// SAFETY: handle is checked for null and points to a valid PlayerCore by API contract.
70-
let core = unsafe { &mut *handle };
71-
core.scan_local_library(&music_dir);
72+
let core = unsafe { &*handle };
73+
if let Ok(core) = core.lock() {
74+
core.scan_local_library(&music_dir);
75+
} else {
76+
return ERR_OP;
77+
}
7278
OK
7379
}
7480

7581
#[unsafe(no_mangle)]
76-
pub extern "C" fn liplayer_track_count(handle: *mut PlayerCore) -> usize {
82+
pub extern "C" fn liplayer_track_count(handle: *mut PlayerHandle) -> usize {
7783
if handle.is_null() {
7884
return 0;
7985
}
8086
// SAFETY: handle is checked for null and points to a valid PlayerCore by API contract.
81-
let core = unsafe { &mut *handle };
82-
core.list_tracks().len()
87+
let core = unsafe { &*handle };
88+
core.lock().map(|c| c.list_tracks().len()).unwrap_or(0)
8389
}
8490

8591
#[unsafe(no_mangle)]
86-
pub extern "C" fn liplayer_list_tracks_json(handle: *mut PlayerCore) -> *mut c_char {
92+
pub extern "C" fn liplayer_list_tracks_json(handle: *mut PlayerHandle) -> *mut c_char {
8793
if handle.is_null() {
8894
return null_mut();
8995
}
9096
// SAFETY: handle is checked for null and points to a valid PlayerCore by API contract.
91-
let core = unsafe { &mut *handle };
92-
let tracks = core.list_tracks();
93-
let json = match serde_json::to_string(&tracks.iter().map(|t| &t.path).collect::<Vec<_>>()) {
97+
let core = unsafe { &*handle };
98+
let tracks = match core.lock() {
99+
Ok(c) => c.list_tracks(),
100+
Err(_) => return null_mut(),
101+
};
102+
let json = match serde_json::to_string(&tracks) {
94103
Ok(v) => v,
95104
Err(_) => return null_mut(),
96105
};
@@ -112,69 +121,87 @@ pub extern "C" fn liplayer_string_free(s: *mut c_char) {
112121
}
113122

114123
#[unsafe(no_mangle)]
115-
pub extern "C" fn liplayer_play_track_at(handle: *mut PlayerCore, index: usize) -> c_int {
124+
pub extern "C" fn liplayer_play_track_at(handle: *mut PlayerHandle, index: usize) -> c_int {
116125
if handle.is_null() {
117126
return ERR_NULL;
118127
}
119128
// SAFETY: handle is checked for null and points to a valid PlayerCore by API contract.
120-
let core = unsafe { &mut *handle };
121-
match core.play_track_at(index) {
122-
Ok(()) => OK,
129+
let core = unsafe { &*handle };
130+
match core.lock() {
131+
Ok(mut c) => c.play_track_at(index).map(|_| OK).unwrap_or(ERR_OP),
123132
Err(_) => ERR_OP,
124133
}
125134
}
126135

127136
#[unsafe(no_mangle)]
128-
pub extern "C" fn liplayer_stop(handle: *mut PlayerCore) -> c_int {
137+
pub extern "C" fn liplayer_stop(handle: *mut PlayerHandle) -> c_int {
129138
if handle.is_null() {
130139
return ERR_NULL;
131140
}
132141
// SAFETY: handle is checked for null and points to a valid PlayerCore by API contract.
133-
let core = unsafe { &mut *handle };
134-
core.stop();
135-
OK
142+
let core = unsafe { &*handle };
143+
if let Ok(mut c) = core.lock() {
144+
c.stop();
145+
OK
146+
} else {
147+
ERR_OP
148+
}
136149
}
137150

138151
#[unsafe(no_mangle)]
139-
pub extern "C" fn liplayer_pause(handle: *mut PlayerCore) -> c_int {
152+
pub extern "C" fn liplayer_pause(handle: *mut PlayerHandle) -> c_int {
140153
if handle.is_null() {
141154
return ERR_NULL;
142155
}
143156
// SAFETY: handle is checked for null and points to a valid PlayerCore by API contract.
144-
let core = unsafe { &mut *handle };
145-
core.pause();
146-
OK
157+
let core = unsafe { &*handle };
158+
if let Ok(mut c) = core.lock() {
159+
c.pause();
160+
OK
161+
} else {
162+
ERR_OP
163+
}
147164
}
148165

149166
#[unsafe(no_mangle)]
150-
pub extern "C" fn liplayer_resume(handle: *mut PlayerCore) -> c_int {
167+
pub extern "C" fn liplayer_resume(handle: *mut PlayerHandle) -> c_int {
151168
if handle.is_null() {
152169
return ERR_NULL;
153170
}
154171
// SAFETY: handle is checked for null and points to a valid PlayerCore by API contract.
155-
let core = unsafe { &mut *handle };
156-
core.resume();
157-
OK
172+
let core = unsafe { &*handle };
173+
if let Ok(mut c) = core.lock() {
174+
c.resume();
175+
OK
176+
} else {
177+
ERR_OP
178+
}
158179
}
159180

160181
#[unsafe(no_mangle)]
161-
pub extern "C" fn liplayer_elapsed_millis(handle: *mut PlayerCore) -> c_ulonglong {
182+
pub extern "C" fn liplayer_elapsed_millis(handle: *mut PlayerHandle) -> c_ulonglong {
162183
if handle.is_null() {
163184
return 0;
164185
}
165186
// SAFETY: handle is checked for null and points to a valid PlayerCore by API contract.
166-
let core = unsafe { &mut *handle };
167-
core.elapsed_millis() as c_ulonglong
187+
let core = unsafe { &*handle };
188+
core.lock()
189+
.map(|c| c.elapsed_millis() as c_ulonglong)
190+
.unwrap_or(0)
168191
}
169192

170193
#[unsafe(no_mangle)]
171-
pub extern "C" fn liplayer_state(handle: *mut PlayerCore) -> c_int {
194+
pub extern "C" fn liplayer_state(handle: *mut PlayerHandle) -> c_int {
172195
if handle.is_null() {
173196
return ERR_NULL;
174197
}
175198
// SAFETY: handle is checked for null and points to a valid PlayerCore by API contract.
176-
let core = unsafe { &mut *handle };
177-
match core.state() {
199+
let core = unsafe { &*handle };
200+
let state = match core.lock() {
201+
Ok(c) => c.state(),
202+
Err(_) => return ERR_OP,
203+
};
204+
match state {
178205
PlayerState::Idle => 0,
179206
PlayerState::Playing => 1,
180207
PlayerState::Paused => 2,

0 commit comments

Comments
 (0)