Skip to main content

virtualdj_plugin_sdk/
lib.rs

1//! VirtualDJ Rust SDK - Safe API Bindings
2//! 
3//! This module provides a high-level, safe Rust API for developing VirtualDJ plugins.
4//! It wraps the low-level FFI bindings with proper error handling and memory safety.
5
6pub mod ffi;
7
8use std::ffi::{CStr, CString};
9use std::fmt;
10
11/// VirtualDJ plugin error type
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum PluginError {
14    /// Operation succeeded
15    Ok,
16    /// General failure
17    Fail,
18    /// Not implemented
19    NotImplemented,
20    /// Null pointer error
21    NullPointer,
22}
23
24impl fmt::Display for PluginError {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            PluginError::Ok => write!(f, "Success"),
28            PluginError::Fail => write!(f, "General failure"),
29            PluginError::NotImplemented => write!(f, "Not implemented"),
30            PluginError::NullPointer => write!(f, "Null pointer"),
31        }
32    }
33}
34
35impl std::error::Error for PluginError {}
36
37impl From<ffi::HRESULT> for PluginError {
38    fn from(hr: ffi::HRESULT) -> Self {
39        match hr {
40            ffi::S_OK => PluginError::Ok,
41            ffi::E_NOTIMPL => PluginError::NotImplemented,
42            ffi::E_FAIL => PluginError::Fail,
43            _ => PluginError::Fail,
44        }
45    }
46}
47
48/// Result type for plugin operations
49pub type Result<T> = std::result::Result<T, PluginError>;
50
51/// Plugin context that provides access to VirtualDJ API functions
52/// 
53/// This struct holds the plugin pointer and callbacks, allowing safe access to
54/// VirtualDJ state information like track metadata, position, BPM, etc.
55/// 
56/// # Example
57/// 
58/// ```ignore
59/// let context = PluginContext::new(plugin_ptr, &callbacks);
60/// let title = context.get_info_string("deck 1 get_title")?;
61/// let position = context.get_info_double("deck 1 get_position")?;
62/// ```
63pub struct PluginContext {
64    plugin: *mut ffi::VdjPlugin,
65    callbacks: *const ffi::VdjCallbacks,
66}
67
68impl PluginContext {
69    /// Create a new plugin context
70    /// 
71    /// # Arguments
72    /// * `plugin` - Pointer to the VdjPlugin struct
73    /// * `callbacks` - Reference to the VdjCallbacks struct
74    /// 
75    /// # Safety
76    /// The plugin pointer must be valid and the callbacks must remain valid
77    /// for the lifetime of this context.
78    pub fn new(plugin: *mut ffi::VdjPlugin, callbacks: &ffi::VdjCallbacks) -> Self {
79        PluginContext {
80            plugin,
81            callbacks: callbacks as *const ffi::VdjCallbacks,
82        }
83    }
84
85    /// Query VirtualDJ for a double/numeric value
86    /// 
87    /// # Arguments
88    /// * `command` - The VDJ script command (e.g., "deck 1 get_position")
89    /// 
90    /// # Returns
91    /// A Result containing the numeric value, or an error if the command fails
92    pub fn get_info_double(&self, command: &str) -> Result<f64> {
93        if self.plugin.is_null() || self.callbacks.is_null() {
94            return Err(PluginError::NullPointer);
95        }
96
97        let c_command = std::ffi::CString::new(command)
98            .map_err(|_| PluginError::Fail)?;
99        
100        let mut result: f64 = 0.0;
101        
102        let hr = unsafe {
103            ((*self.callbacks).get_info)(
104                self.plugin,
105                c_command.as_ptr() as *const u8,
106                &mut result as *mut f64,
107            )
108        };
109
110        if hr == ffi::S_OK {
111            Ok(result)
112        } else {
113            Err(PluginError::from(hr))
114        }
115    }
116
117    /// Query VirtualDJ for a string value
118    /// 
119    /// # Arguments
120    /// * `command` - The VDJ script command (e.g., "deck 1 get_title")
121    /// 
122    /// # Returns
123    /// A Result containing the string value, or an error if the command fails
124    pub fn get_info_string(&self, command: &str) -> Result<String> {
125        if self.plugin.is_null() || self.callbacks.is_null() {
126            return Err(PluginError::NullPointer);
127        }
128
129        let c_command = std::ffi::CString::new(command)
130            .map_err(|_| PluginError::Fail)?;
131        
132        let mut output: [u8; 1024] = [0; 1024];
133        
134        let hr = unsafe {
135            ((*self.callbacks).get_string_info)(
136                self.plugin,
137                c_command.as_ptr() as *const u8,
138                output.as_mut_ptr(),
139                output.len() as i32,
140            )
141        };
142
143        if hr == ffi::S_OK {
144            let c_str = unsafe { CStr::from_ptr(output.as_ptr() as *const i8) };
145            Ok(c_str.to_string_lossy().into_owned())
146        } else {
147            Err(PluginError::from(hr))
148        }
149    }
150
151    /// Send a command to VirtualDJ
152    /// 
153    /// # Arguments
154    /// * `command` - The VDJ script command (e.g., "deck 1 play")
155    /// 
156    /// # Returns
157    /// Ok(()) if successful, or an error if the command fails
158    pub fn send_command(&self, command: &str) -> Result<()> {
159        if self.plugin.is_null() || self.callbacks.is_null() {
160            return Err(PluginError::NullPointer);
161        }
162
163        let c_command = std::ffi::CString::new(command)
164            .map_err(|_| PluginError::Fail)?;
165        
166        let hr = unsafe {
167            ((*self.callbacks).send_command)(
168                self.plugin,
169                c_command.as_ptr() as *const u8,
170            )
171        };
172
173        if hr == ffi::S_OK {
174            Ok(())
175        } else {
176            Err(PluginError::from(hr))
177        }
178    }
179}
180
181/// Plugin information
182#[derive(Debug, Clone)]
183pub struct PluginInfo {
184    pub name: String,
185    pub author: String,
186    pub description: String,
187    pub version: String,
188    pub flags: u32,
189}
190
191impl PluginInfo {
192    fn from_ffi(ffi_info: &ffi::VdjPluginInfo) -> Result<Self> {
193        let name = unsafe { CStr::from_ptr(ffi_info.plugin_name as *const i8) }
194            .to_string_lossy()
195            .into_owned();
196        let author = unsafe { CStr::from_ptr(ffi_info.author as *const i8) }
197            .to_string_lossy()
198            .into_owned();
199        let description = unsafe { CStr::from_ptr(ffi_info.description as *const i8) }
200            .to_string_lossy()
201            .into_owned();
202        let version = unsafe { CStr::from_ptr(ffi_info.version as *const i8) }
203            .to_string_lossy()
204            .into_owned();
205
206        Ok(PluginInfo {
207            name,
208            author,
209            description,
210            version,
211            flags: ffi_info.flags,
212        })
213    }
214}
215
216/// Base plugin trait that all plugin types implement
217pub trait PluginBase {
218    /// Called when the plugin is loaded
219    fn on_load(&mut self) -> Result<()> {
220        Ok(())
221    }
222
223    /// Get plugin information
224    fn get_info(&self) -> PluginInfo {
225        PluginInfo {
226            name: "VirtualDJ Plugin".to_string(),
227            author: "Plugin Developer".to_string(),
228            description: "A VirtualDJ plugin".to_string(),
229            version: "1.0.0".to_string(),
230            flags: 0,
231        }
232    }
233
234    /// Called when a parameter is changed
235    fn on_parameter(&mut self, id: i32) -> Result<()> {
236        Ok(())
237    }
238
239    /// Get string representation of a parameter
240    fn on_get_parameter_string(&self, id: i32) -> Result<String> {
241        Err(PluginError::NotImplemented)
242    }
243}
244
245/// DSP plugin trait for audio effect plugins
246pub trait DspPlugin: PluginBase {
247    /// Called when the DSP plugin starts
248    fn on_start(&mut self) -> Result<()> {
249        Ok(())
250    }
251
252    /// Called when the DSP plugin stops
253    fn on_stop(&mut self) -> Result<()> {
254        Ok(())
255    }
256
257    /// Process audio samples (stereo, so buffer is 2*nb samples)
258    fn on_process_samples(&mut self, buffer: &mut [f32]) -> Result<()>;
259
260    /// Get the sample rate
261    fn sample_rate(&self) -> i32 {
262        44100
263    }
264
265    /// Get the song BPM
266    fn song_bpm(&self) -> i32 {
267        120
268    }
269
270    /// Get the song position in beats
271    fn song_pos_beats(&self) -> f64 {
272        0.0
273    }
274}
275
276/// Buffer DSP plugin trait for buffer manipulation
277pub trait BufferDspPlugin: PluginBase {
278    /// Called when the plugin starts
279    fn on_start(&mut self) -> Result<()> {
280        Ok(())
281    }
282
283    /// Called when the plugin stops
284    fn on_stop(&mut self) -> Result<()> {
285        Ok(())
286    }
287
288    /// Get song buffer at the specified position
289    fn on_get_song_buffer(&mut self, song_pos: i32, nb: i32) -> Option<&[i16]>;
290
291    /// Get the sample rate
292    fn sample_rate(&self) -> i32 {
293        44100
294    }
295
296    /// Get the song BPM
297    fn song_bpm(&self) -> i32 {
298        120
299    }
300
301    /// Get the song position in samples
302    fn song_pos(&self) -> i32 {
303        0
304    }
305
306    /// Get the song position in beats
307    fn song_pos_beats(&self) -> f64 {
308        0.0
309    }
310}
311
312/// Position DSP plugin trait for position manipulation
313pub trait PositionDspPlugin: PluginBase {
314    /// Called when the plugin starts
315    fn on_start(&mut self) -> Result<()> {
316        Ok(())
317    }
318
319    /// Called when the plugin stops
320    fn on_stop(&mut self) -> Result<()> {
321        Ok(())
322    }
323
324    /// Transform position (can modify songPos, videoPos, volume)
325    fn on_transform_position(
326        &mut self,
327        song_pos: &mut f64,
328        video_pos: &mut f64,
329        volume: &mut f32,
330        src_volume: &mut f32,
331    ) -> Result<()>;
332
333    /// Process audio samples
334    fn on_process_samples(&mut self, buffer: &mut [f32]) -> Result<()> {
335        Ok(())
336    }
337
338    /// Get the sample rate
339    fn sample_rate(&self) -> i32 {
340        44100
341    }
342
343    /// Get the song BPM
344    fn song_bpm(&self) -> i32 {
345        120
346    }
347
348    /// Get the song position in samples
349    fn song_pos(&self) -> i32 {
350        0
351    }
352
353    /// Get the song position in beats
354    fn song_pos_beats(&self) -> f64 {
355        0.0
356    }
357}
358
359/// Video FX plugin trait
360pub trait VideoFxPlugin: PluginBase {
361    /// Called when the plugin starts
362    fn on_start(&mut self) -> Result<()> {
363        Ok(())
364    }
365
366    /// Called when the plugin stops
367    fn on_stop(&mut self) -> Result<()> {
368        Ok(())
369    }
370
371    /// Draw the video frame
372    fn on_draw(&mut self) -> Result<()>;
373
374    /// Called when graphics device is initialized
375    fn on_device_init(&mut self) -> Result<()> {
376        Ok(())
377    }
378
379    /// Called when graphics device is closed
380    fn on_device_close(&mut self) -> Result<()> {
381        Ok(())
382    }
383
384    /// Process audio samples for visualization
385    fn on_audio_samples(&mut self, buffer: &[f32]) -> Result<()> {
386        Ok(())
387    }
388
389    /// Get video width
390    fn width(&self) -> i32 {
391        1920
392    }
393
394    /// Get video height
395    fn height(&self) -> i32 {
396        1080
397    }
398
399    /// Get the sample rate
400    fn sample_rate(&self) -> i32 {
401        44100
402    }
403
404    /// Get the song BPM
405    fn song_bpm(&self) -> i32 {
406        120
407    }
408
409    /// Get the song position in beats
410    fn song_pos_beats(&self) -> f64 {
411        0.0
412    }
413}
414
415/// Video transition plugin trait
416pub trait VideoTransitionPlugin: PluginBase {
417    /// Draw the video transition
418    fn on_draw(&mut self, crossfader: f32) -> Result<()>;
419
420    /// Called when graphics device is initialized
421    fn on_device_init(&mut self) -> Result<()> {
422        Ok(())
423    }
424
425    /// Called when graphics device is closed
426    fn on_device_close(&mut self) -> Result<()> {
427        Ok(())
428    }
429
430    /// Get video width
431    fn width(&self) -> i32 {
432        1920
433    }
434
435    /// Get video height
436    fn height(&self) -> i32 {
437        1080
438    }
439
440    /// Get the sample rate
441    fn sample_rate(&self) -> i32 {
442        44100
443    }
444
445    /// Get the song BPM
446    fn song_bpm(&self) -> i32 {
447        120
448    }
449
450    /// Get the song position in beats
451    fn song_pos_beats(&self) -> f64 {
452        0.0
453    }
454}
455
456/// Online source plugin trait
457pub trait OnlineSourcePlugin: PluginBase {
458    /// Check if user is logged in
459    fn is_logged(&self) -> Result<bool> {
460        Ok(false)
461    }
462
463    /// Handle login
464    fn on_login(&mut self) -> Result<()> {
465        Ok(())
466    }
467
468    /// Handle logout
469    fn on_logout(&mut self) -> Result<()> {
470        Ok(())
471    }
472
473    /// Search for tracks
474    fn on_search(&mut self, search: &str) -> Result<Vec<SearchResult>> {
475        Ok(Vec::new())
476    }
477
478    /// Cancel ongoing search
479    fn on_search_cancel(&mut self) -> Result<()> {
480        Ok(())
481    }
482}
483
484/// Search result for online sources
485#[derive(Debug, Clone)]
486pub struct SearchResult {
487    pub unique_id: String,
488    pub title: String,
489    pub artist: String,
490    pub genre: Option<String>,
491    pub bpm: Option<f32>,
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497
498    #[test]
499    fn test_error_conversion() {
500        let err: PluginError = ffi::E_FAIL.into();
501        assert_eq!(err, PluginError::Fail);
502    }
503}