Skip to main content

aetherdsp_juce_bridge/
lib.rs

1//! # AetherDSP JUCE Bridge
2//!
3//! C FFI bridge for integrating AetherDSP's world music tuning systems with JUCE plugins.
4//!
5//! ## Quick Start
6//!
7//! ```cpp
8//! #include "aetherdsp_juce_bridge.h"
9//!
10//! // Create Ethiopian Tizita tuning
11//! AetherTuningTable* tuning = aether_tuning_ethiopian_tizita();
12//!
13//! // Get frequency for MIDI note 60 (Middle C)
14//! float freq;
15//! aether_tuning_get_frequency(tuning, 60, &freq);
16//!
17//! // Use in your oscillator
18//! myOscillator.setFrequency(freq);
19//!
20//! // Clean up
21//! aether_tuning_free(tuning);
22//! ```
23
24use aether_midi::tuning::TuningTable;
25use std::ffi::c_char;
26
27// ============================================================================
28// TYPE DEFINITIONS
29// ============================================================================
30
31/// Opaque handle to a tuning table
32#[repr(C)]
33pub struct AetherTuningTable {
34    _private: [u8; 0],
35}
36
37/// Result code for API calls
38#[repr(C)]
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum AetherResult {
41    Ok = 0,
42    ErrorNullPointer = 1,
43    ErrorInvalidNote = 2,
44    ErrorUnknown = 99,
45}
46
47// ============================================================================
48// INTERNAL CONVERSION
49// ============================================================================
50
51unsafe fn tuning_from_handle(handle: *const AetherTuningTable) -> Option<&'static TuningTable> {
52    if handle.is_null() {
53        return None;
54    }
55    Some(&*(handle as *const TuningTable))
56}
57
58// Standard concert A frequency (A4 = 440 Hz)
59const CONCERT_A: f32 = 440.0;
60
61// ============================================================================
62// TUNING SYSTEMS
63// ============================================================================
64
65/// Create Ethiopian Tizita major tuning (pentatonic, characteristic of Ethiopian blues)
66#[no_mangle]
67pub extern "C" fn aether_tuning_ethiopian_tizita() -> *mut AetherTuningTable {
68    let tuning = Box::new(TuningTable::ethiopian_tizita(CONCERT_A));
69    Box::into_raw(tuning) as *mut AetherTuningTable
70}
71
72/// Create Ethiopian Tizita minor tuning (nostalgic, melancholic pentatonic variant)
73#[no_mangle]
74pub extern "C" fn aether_tuning_ethiopian_tizita_minor() -> *mut AetherTuningTable {
75    let tuning = Box::new(TuningTable::ethiopian_tizita_minor(CONCERT_A));
76    Box::into_raw(tuning) as *mut AetherTuningTable
77}
78
79/// Create Ethiopian Bati minor tuning (standard minor pentatonic variant)
80#[no_mangle]
81pub extern "C" fn aether_tuning_ethiopian_bati() -> *mut AetherTuningTable {
82    let tuning = Box::new(TuningTable::ethiopian_bati(CONCERT_A));
83    Box::into_raw(tuning) as *mut AetherTuningTable
84}
85
86/// Create Ethiopian Bati major tuning (bright, uplifting pentatonic variant)
87#[no_mangle]
88pub extern "C" fn aether_tuning_ethiopian_bati_major() -> *mut AetherTuningTable {
89    let tuning = Box::new(TuningTable::ethiopian_bati_major(CONCERT_A));
90    Box::into_raw(tuning) as *mut AetherTuningTable
91}
92
93/// Create Ethiopian Ambassel tuning (pentatonic with flat 2nd)
94#[no_mangle]
95pub extern "C" fn aether_tuning_ethiopian_ambassel() -> *mut AetherTuningTable {
96    let tuning = Box::new(TuningTable::ethiopian_ambassel(CONCERT_A));
97    Box::into_raw(tuning) as *mut AetherTuningTable
98}
99
100/// Create Ethiopian Anchihoye tuning (pentatonic without 3rd degree)
101#[no_mangle]
102pub extern "C" fn aether_tuning_ethiopian_anchihoye() -> *mut AetherTuningTable {
103    let tuning = Box::new(TuningTable::ethiopian_anchihoye(CONCERT_A));
104    Box::into_raw(tuning) as *mut AetherTuningTable
105}
106
107/// Create Arabic Maqam Rast tuning (quarter-tone flats on 3rd and 7th)
108#[no_mangle]
109pub extern "C" fn aether_tuning_arabic_rast() -> *mut AetherTuningTable {
110    let tuning = Box::new(TuningTable::arabic_maqam_rast(CONCERT_A));
111    Box::into_raw(tuning) as *mut AetherTuningTable
112}
113
114/// Create Arabic Maqam Bayati tuning (half-flat on 2nd degree)
115#[no_mangle]
116pub extern "C" fn aether_tuning_arabic_bayati() -> *mut AetherTuningTable {
117    let tuning = Box::new(TuningTable::arabic_maqam_bayati(CONCERT_A));
118    Box::into_raw(tuning) as *mut AetherTuningTable
119}
120
121/// Create Arabic Maqam Hijaz tuning (augmented 2nd between 2nd and 3rd degrees)
122#[no_mangle]
123pub extern "C" fn aether_tuning_arabic_hijaz() -> *mut AetherTuningTable {
124    let tuning = Box::new(TuningTable::arabic_maqam_hijaz(CONCERT_A));
125    Box::into_raw(tuning) as *mut AetherTuningTable
126}
127
128/// Create Indian Raga Yaman tuning (raised 4th, Kalyan thaat)
129#[no_mangle]
130pub extern "C" fn aether_tuning_indian_yaman() -> *mut AetherTuningTable {
131    let tuning = Box::new(TuningTable::indian_raga_yaman(CONCERT_A));
132    Box::into_raw(tuning) as *mut AetherTuningTable
133}
134
135/// Create Gamelan Slendro tuning (5-tone Javanese scale)
136#[no_mangle]
137pub extern "C" fn aether_tuning_gamelan_slendro() -> *mut AetherTuningTable {
138    let tuning = Box::new(TuningTable::gamelan_slendro(CONCERT_A));
139    Box::into_raw(tuning) as *mut AetherTuningTable
140}
141
142/// Create Gamelan Slendro Stretched tuning (1210-cent octaves, ethnomusicologically accurate)
143#[no_mangle]
144pub extern "C" fn aether_tuning_gamelan_slendro_stretched() -> *mut AetherTuningTable {
145    let tuning = Box::new(TuningTable::gamelan_slendro_stretched(CONCERT_A));
146    Box::into_raw(tuning) as *mut AetherTuningTable
147}
148
149/// Create Gamelan Pelog tuning (7-tone Javanese scale with unequal intervals)
150#[no_mangle]
151pub extern "C" fn aether_tuning_gamelan_pelog() -> *mut AetherTuningTable {
152    let tuning = Box::new(TuningTable::gamelan_pelog(CONCERT_A));
153    Box::into_raw(tuning) as *mut AetherTuningTable
154}
155
156/// Create Just Intonation (5-limit) tuning (pure thirds and fifths)
157#[no_mangle]
158pub extern "C" fn aether_tuning_just_intonation() -> *mut AetherTuningTable {
159    let tuning = Box::new(TuningTable::just_intonation(CONCERT_A));
160    Box::into_raw(tuning) as *mut AetherTuningTable
161}
162
163/// Create Just Intonation (7-limit) tuning (septimal intervals for blues and barbershop)
164#[no_mangle]
165pub extern "C" fn aether_tuning_just_intonation_7_limit() -> *mut AetherTuningTable {
166    let tuning = Box::new(TuningTable::just_intonation_7_limit(CONCERT_A));
167    Box::into_raw(tuning) as *mut AetherTuningTable
168}
169
170/// Create standard 12-TET tuning (equal temperament, 12 equal divisions of octave)
171#[no_mangle]
172pub extern "C" fn aether_tuning_equal_temperament() -> *mut AetherTuningTable {
173    let tuning = Box::new(TuningTable::equal_temperament(CONCERT_A));
174    Box::into_raw(tuning) as *mut AetherTuningTable
175}
176
177/// Free a tuning table
178///
179/// # Safety
180/// `tuning` must be a valid handle from an `aether_tuning_*()` function.
181/// Do not use the handle after calling this function.
182#[no_mangle]
183pub unsafe extern "C" fn aether_tuning_free(tuning: *mut AetherTuningTable) {
184    if !tuning.is_null() {
185        let _ = Box::from_raw(tuning as *mut TuningTable);
186    }
187}
188
189/// Get the frequency in Hz for a given MIDI note from a tuning table
190///
191/// # Arguments
192/// * `tuning` - Tuning table handle
193/// * `midi_note` - MIDI note number (0-127, where 60 = Middle C)
194/// * `out_frequency` - Pointer to receive the frequency in Hz
195///
196/// # Returns
197/// AetherResult::Ok on success, error code on failure
198///
199/// # Safety
200/// `tuning` and `out_frequency` must be valid pointers
201#[no_mangle]
202pub unsafe extern "C" fn aether_tuning_get_frequency(
203    tuning: *const AetherTuningTable,
204    midi_note: u8,
205    out_frequency: *mut f32,
206) -> AetherResult {
207    if out_frequency.is_null() {
208        return AetherResult::ErrorNullPointer;
209    }
210
211    let tuning = match tuning_from_handle(tuning) {
212        Some(t) => t,
213        None => return AetherResult::ErrorNullPointer,
214    };
215
216    *out_frequency = tuning.frequency(midi_note);
217    AetherResult::Ok
218}
219
220/// Get the complete frequency table (128 values, one for each MIDI note)
221///
222/// # Arguments
223/// * `tuning` - Tuning table handle
224/// * `out_frequencies` - Pointer to array of 128 floats to receive frequencies
225///
226/// # Returns
227/// AetherResult::Ok on success
228///
229/// # Safety
230/// `out_frequencies` must point to an array of at least 128 floats
231#[no_mangle]
232pub unsafe extern "C" fn aether_tuning_get_all_frequencies(
233    tuning: *const AetherTuningTable,
234    out_frequencies: *mut f32,
235) -> AetherResult {
236    if out_frequencies.is_null() {
237        return AetherResult::ErrorNullPointer;
238    }
239
240    let tuning = match tuning_from_handle(tuning) {
241        Some(t) => t,
242        None => return AetherResult::ErrorNullPointer,
243    };
244
245    let out_slice = std::slice::from_raw_parts_mut(out_frequencies, 128);
246    for (i, freq) in out_slice.iter_mut().enumerate() {
247        *freq = tuning.frequency(i as u8);
248    }
249
250    AetherResult::Ok
251}
252
253// ============================================================================
254// VERSION INFO
255// ============================================================================
256
257/// Get the AetherDSP version string
258///
259/// # Returns
260/// Null-terminated version string (e.g., "0.1.6")
261/// Do not free this pointer - it points to static data
262#[no_mangle]
263pub extern "C" fn aether_version() -> *const c_char {
264    concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
265}
266
267/// Get the number of available tuning systems
268///
269/// # Returns
270/// The total count of built-in tuning systems (currently 17)
271#[no_mangle]
272pub extern "C" fn aether_tuning_count() -> u32 {
273    17
274}
275
276// ============================================================================
277// TESTS
278// ============================================================================
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_tuning_lifecycle() {
286        unsafe {
287            let tuning = aether_tuning_ethiopian_tizita();
288            assert!(!tuning.is_null());
289
290            let mut freq = 0.0f32;
291            let result = aether_tuning_get_frequency(tuning, 60, &mut freq);
292            assert_eq!(result, AetherResult::Ok);
293            assert!(freq > 200.0 && freq < 300.0); // Middle C should be ~261 Hz
294
295            aether_tuning_free(tuning);
296        }
297    }
298
299    #[test]
300    fn test_all_tuning_systems() {
301        unsafe {
302            let tunings = [
303                aether_tuning_ethiopian_tizita(),
304                aether_tuning_ethiopian_bati(),
305                aether_tuning_ethiopian_ambassel(),
306                aether_tuning_arabic_rast(),
307                aether_tuning_arabic_bayati(),
308                aether_tuning_arabic_hijaz(),
309                aether_tuning_indian_yaman(),
310                aether_tuning_gamelan_slendro(),
311                aether_tuning_gamelan_slendro_stretched(),
312                aether_tuning_gamelan_pelog(),
313                aether_tuning_just_intonation(),
314                aether_tuning_just_intonation_7_limit(),
315                aether_tuning_equal_temperament(),
316            ];
317
318            for tuning in &tunings {
319                assert!(!tuning.is_null());
320
321                let mut freq = 0.0f32;
322                let result = aether_tuning_get_frequency(*tuning, 60, &mut freq);
323                assert_eq!(result, AetherResult::Ok);
324                assert!(freq > 0.0);
325            }
326
327            for tuning in tunings {
328                aether_tuning_free(tuning);
329            }
330        }
331    }
332
333    #[test]
334    fn test_get_all_frequencies() {
335        unsafe {
336            let tuning = aether_tuning_arabic_hijaz();
337            let mut frequencies = [0.0f32; 128];
338
339            let result = aether_tuning_get_all_frequencies(tuning, frequencies.as_mut_ptr());
340            assert_eq!(result, AetherResult::Ok);
341
342            // Check that all frequencies are reasonable
343            for (i, freq) in frequencies.iter().enumerate() {
344                assert!(*freq > 0.0, "Note {} has invalid frequency", i);
345            }
346
347            aether_tuning_free(tuning);
348        }
349    }
350
351    #[test]
352    fn test_version() {
353        let version = aether_version();
354        assert!(!version.is_null());
355    }
356
357    #[test]
358    fn test_tuning_count() {
359        assert_eq!(aether_tuning_count(), 17);
360    }
361}