fluid_xenth/
lib.rs

1use std::{
2    error::Error,
3    fmt::{self, Display, Formatter},
4    hash::Hash,
5    sync::mpsc::{self, Receiver, Sender},
6};
7
8use mpsc::SendError;
9use oxisynth::{MidiEvent, OxiError, SettingsError, SynthDescriptor, Tuning};
10use tune::{
11    note::Note,
12    pitch::{Pitch, Ratio},
13    tuner::{AotTuner, GroupBy, JitTuner, PoolingMode, TunableSynth},
14    tuning::KeyboardMapping,
15};
16
17pub use oxisynth;
18pub use tune;
19
20/// Creates a connected ([`Xenth`], [`AotXenthControl`]) pair.
21///
22/// The [`Xenth`] part is intended to be used in the audio thread.
23/// The [`AotXenthControl`] part can be used anywhere in your application to control the behavior of the [`Xenth`] part.
24pub fn create_aot<K>(
25    desc: SynthDescriptor,
26    per_semitone_polyphony: u8,
27) -> Result<(Xenth, AotXenthControl<K>), SettingsError> {
28    let (xenth, tuners) =
29        create_internal(desc, per_semitone_polyphony, |synth| AotTuner::start(synth))?;
30    Ok((xenth, AotXenthControl { tuners }))
31}
32
33/// Creates a connected ([`Xenth`], [`JitXenthControl`]) pair.
34///
35/// The [`Xenth`] part is intended to be used in the audio thread.
36/// The [`JitXenthControl`] part can be used anywhere in your application to control the behavior of the [`Xenth`] part.
37pub fn create_jit<K>(
38    desc: SynthDescriptor,
39    per_semitone_polyphony: u8,
40) -> Result<(Xenth, JitXenthControl<K>), SettingsError> {
41    let (xenth, tuners) = create_internal(desc, per_semitone_polyphony, |synth| {
42        JitTuner::start(synth, PoolingMode::Stop)
43    })?;
44    Ok((xenth, JitXenthControl { tuners }))
45}
46
47/// Creates a [`Xenth`] instance and several connected [`TunableFluid`] instances.
48///
49/// The [`Xenth`] part is intended to be used in the audio thread.
50/// The [`TunableFluid`] parts can be used anywhere in your application to control the behavior of the [`Xenth`] part.
51pub fn create<K>(
52    desc: SynthDescriptor,
53    per_semitone_polyphony: u8,
54) -> Result<(Xenth, Vec<TunableFluid>), SettingsError> {
55    let (xenth, tuners) = create_internal(desc, per_semitone_polyphony, |synth| synth)?;
56    Ok((xenth, tuners))
57}
58
59fn create_internal<T, C: FnMut(TunableFluid) -> T>(
60    mut desc: SynthDescriptor,
61    polyphony: u8,
62    mut xenth_control_creator: C,
63) -> Result<(Xenth, Vec<T>), SettingsError> {
64    desc.drums_channel_active = false;
65    let synth = oxisynth::Synth::new(desc)?;
66
67    let (sender, receiver) = mpsc::channel();
68
69    let tuners = (0..synth.count_midi_channels())
70        .collect::<Vec<_>>()
71        .chunks_exact(usize::from(polyphony))
72        .map(|chunk| {
73            xenth_control_creator(TunableFluid {
74                sender: sender.clone(),
75                offset: chunk[0],
76                polyphony: usize::from(polyphony),
77            })
78        })
79        .collect();
80
81    let xenth = Xenth { synth, receiver };
82
83    Ok((xenth, tuners))
84}
85
86/// The synthesizing end to be used in the audio thread.
87pub struct Xenth {
88    synth: oxisynth::Synth,
89    receiver: Receiver<Command>,
90}
91
92impl Xenth {
93    /// Get readable access to the internal [`oxisynth::Synth`] instance in order to query data.
94    pub fn synth(&self) -> &oxisynth::Synth {
95        &self.synth
96    }
97
98    /// Get writeable access to the internal [`oxisynth::Synth`] instance in order to configure its behavior.
99    ///
100    /// Refrain from modifying the tuning of the internal [`oxisynth::Synth`] instance as `fluid-xenth` will manage the tuning for you.
101    pub fn synth_mut(&mut self) -> &mut oxisynth::Synth {
102        &mut self.synth
103    }
104
105    /// Flushes all commands and uses the internal [`oxisynth::Synth`] instance to create a stream of synthesized audio samples.
106    pub fn read(&mut self) -> Result<impl FnMut() -> (f32, f32) + '_, OxiError> {
107        for command in self.receiver.try_iter() {
108            command(&mut self.synth)?;
109        }
110        Ok(|| self.synth.read_next())
111    }
112
113    /// Flushes all commands and uses the internal [`oxisynth::Synth`] instance to write the synthesized audio using the given `write_callback`.
114    pub fn write(
115        &mut self,
116        len: usize,
117        mut write_callback: impl FnMut((f32, f32)),
118    ) -> Result<(), OxiError> {
119        let mut samples = self.read()?;
120        for _ in 0..len {
121            write_callback(samples());
122        }
123        Ok(())
124    }
125}
126
127/// Controls the connected [`Xenth`] instance from any thread using the ahead-of-time tuning model.
128pub struct AotXenthControl<K> {
129    tuners: Vec<AotTuner<K, TunableFluid>>,
130}
131
132impl<K: Copy + Eq + Hash> AotXenthControl<K> {
133    /// Apply the ahead-of-time `tuning` for the given `keys` on the given `xen-channel`.
134    pub fn set_tuning(
135        &mut self,
136        xen_channel: u8,
137        tuning: impl KeyboardMapping<K>,
138        keys: impl IntoIterator<Item = K>,
139    ) -> Result<usize, SendCommandResult> {
140        self.get_tuner(xen_channel).set_tuning(tuning, keys)
141    }
142
143    /// Starts a note with a pitch given by the currently loaded tuning on the given `xen_channel`.
144    pub fn note_on(&mut self, xen_channel: u8, key: K, velocity: u8) -> SendCommandResult {
145        self.get_tuner(xen_channel).note_on(key, velocity)
146    }
147
148    /// Stops the note of the given `key` on the given `xen_channel`.
149    pub fn note_off(&mut self, xen_channel: u8, key: K) -> SendCommandResult {
150        self.get_tuner(xen_channel).note_off(key, 0)
151    }
152
153    /// Sends a key-pressure message to the note with the given `key` on the given `xen_channel`.
154    pub fn key_pressure(&mut self, xen_channel: u8, key: K, pressure: u8) -> SendCommandResult {
155        self.get_tuner(xen_channel).note_attr(key, pressure)
156    }
157
158    /// Sends a channel-based command to the internal [`oxisynth::Synth`] instance.
159    ///
160    /// `fluid-xenth` will map the provided `xen_channel` to the internal real channels of the [`oxisynth::Synth`] instance.
161    ///
162    /// Be aware that calling the "wrong" function (e.g. `add_font`) can put load on the audio thread!
163    pub fn send_command(
164        &mut self,
165        xen_channel: u8,
166        command: impl FnMut(&mut oxisynth::Synth, u8) -> Result<(), OxiError> + Send + 'static,
167    ) -> SendCommandResult {
168        self.get_tuner(xen_channel).global_attr(Box::new(command))
169    }
170
171    fn get_tuner(&mut self, xen_channel: u8) -> &mut AotTuner<K, TunableFluid> {
172        &mut self.tuners[usize::from(xen_channel)]
173    }
174}
175
176/// Controls the connected [`Xenth`] instance from any thread using the just-in-time tuning model.
177pub struct JitXenthControl<K> {
178    tuners: Vec<JitTuner<K, TunableFluid>>,
179}
180
181impl<K: Copy + Eq + Hash> JitXenthControl<K> {
182    /// Starts a note with the given `pitch` on the given `xen_channel`.
183    ///
184    /// `key` is used as identifier for currently sounding notes.
185    pub fn note_on(
186        &mut self,
187        xen_channel: u8,
188        key: K,
189        pitch: Pitch,
190        velocity: u8,
191    ) -> SendCommandResult {
192        self.get_tuner(xen_channel).note_on(key, pitch, velocity)
193    }
194
195    /// Stops the note of the given `key` on the given `xen_channel`.
196    pub fn note_off(&mut self, xen_channel: u8, key: K) -> SendCommandResult {
197        self.get_tuner(xen_channel).note_off(key, 0)
198    }
199
200    /// Sends a key-pressure message to the note with the given `key` on the given `xen_channel`.
201    pub fn key_pressure(&mut self, xen_channel: u8, key: K, pressure: u8) -> SendCommandResult {
202        self.get_tuner(xen_channel).note_attr(key, pressure)
203    }
204
205    /// Sends a channel-based command to the internal [`oxisynth::Synth`] instance.
206    ///
207    /// `fluid-xenth` will map the provided `xen_channel` to the internal real channels of the [`oxisynth::Synth`] instance.
208    ///
209    /// Be aware that calling the "wrong" function (e.g. `add_font`) can put load on the audio thread!
210    pub fn send_command(
211        &mut self,
212        xen_channel: u8,
213        command: impl FnMut(&mut oxisynth::Synth, u8) -> Result<(), OxiError> + Send + 'static,
214    ) -> SendCommandResult {
215        self.get_tuner(xen_channel).global_attr(Box::new(command))
216    }
217
218    fn get_tuner(&mut self, xen_channel: u8) -> &mut JitTuner<K, TunableFluid> {
219        &mut self.tuners[usize::from(xen_channel)]
220    }
221}
222
223/// A [`TunableSynth`] implementation of `fluid-xenth` for later use in an [`AotTuner`] or [`JitTuner`].
224pub struct TunableFluid {
225    sender: Sender<Command>,
226    offset: usize,
227    polyphony: usize,
228}
229
230impl TunableSynth for TunableFluid {
231    type Result = SendCommandResult;
232    type NoteAttr = u8;
233    type GlobalAttr = ChannelCommand;
234
235    fn num_channels(&self) -> usize {
236        self.polyphony
237    }
238
239    fn group_by(&self) -> GroupBy {
240        GroupBy::Note
241    }
242
243    fn notes_detune(
244        &mut self,
245        channel: usize,
246        detuned_notes: &[(Note, Ratio)],
247    ) -> SendCommandResult {
248        let channel = self.get_channel(channel);
249        let mut detunings = Vec::new();
250
251        for &(detuned_note, detuning) in detuned_notes {
252            if let Some(detuned_note) = detuned_note.checked_midi_number() {
253                detunings.push((
254                    u32::from(detuned_note),
255                    Ratio::from_semitones(detuned_note)
256                        .stretched_by(detuning)
257                        .as_cents(),
258                ));
259            }
260        }
261
262        let mut tuning = Tuning::new(0, 0); // Tuning bank and program have no effect
263        self.send_command(move |s| {
264            tuning.tune_notes(&detunings);
265            s.channel_set_tuning(channel, tuning)
266        })
267    }
268
269    fn note_on(&mut self, channel: usize, started_note: Note, velocity: u8) -> SendCommandResult {
270        if let Some(started_note) = started_note.checked_midi_number() {
271            let channel = self.get_channel(channel);
272            self.send_command(move |s| {
273                s.send_event(MidiEvent::NoteOn {
274                    channel,
275                    key: started_note,
276                    vel: velocity,
277                })
278            })?;
279        }
280        Ok(())
281    }
282
283    fn note_off(&mut self, channel: usize, stopped_note: Note, _velocity: u8) -> SendCommandResult {
284        if let Some(stopped_note) = stopped_note.checked_midi_number() {
285            let channel = self.get_channel(channel);
286            self.send_command(move |s| {
287                s.send_event(MidiEvent::NoteOff {
288                    channel,
289                    key: stopped_note,
290                })
291            })?;
292        }
293        Ok(())
294    }
295
296    fn note_attr(
297        &mut self,
298        channel: usize,
299        affected_note: Note,
300        pressure: u8,
301    ) -> SendCommandResult {
302        if let Some(affected_note) = affected_note.checked_midi_number() {
303            let channel = self.get_channel(channel);
304            self.send_command(move |s| {
305                s.send_event(MidiEvent::PolyphonicKeyPressure {
306                    channel,
307                    key: affected_note,
308                    value: pressure,
309                })
310            })?;
311        }
312        Ok(())
313    }
314
315    fn global_attr(&mut self, mut command: ChannelCommand) -> SendCommandResult {
316        let channels = (self.get_channel(0)..).take(self.polyphony);
317        self.send_command(move |s| {
318            for channel in channels {
319                command(s, channel)?;
320            }
321            Ok(())
322        })
323    }
324}
325
326impl TunableFluid {
327    fn send_command(
328        &self,
329        command: impl FnOnce(&mut oxisynth::Synth) -> Result<(), OxiError> + Send + 'static,
330    ) -> SendCommandResult {
331        Ok(self.sender.send(Box::new(command))?)
332    }
333
334    fn get_channel(&self, channel: usize) -> u8 {
335        u8::try_from(self.offset + channel).unwrap()
336    }
337}
338
339pub type SendCommandResult = Result<(), SendCommandError>;
340
341/// Sending a command via [`AotXenthControl`] or [`JitXenthControl`] failed.
342///
343/// This error can only occur if the receiving [`Xenth`] instance has been disconnected / torn down.
344#[derive(Copy, Clone, Debug)]
345pub struct SendCommandError;
346
347impl Display for SendCommandError {
348    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
349        write!(
350            f,
351            "The receiving Xenth instance has been torn down. Is the audio thread still alive?"
352        )
353    }
354}
355
356impl<T> From<SendError<T>> for SendCommandError {
357    fn from(_: SendError<T>) -> Self {
358        SendCommandError
359    }
360}
361
362impl Error for SendCommandError {}
363
364pub type ChannelCommand = Box<dyn FnMut(&mut oxisynth::Synth, u8) -> Result<(), OxiError> + Send>;
365
366type Command = Box<dyn FnOnce(&mut oxisynth::Synth) -> Result<(), OxiError> + Send>;