fyroxed_base/audio/
mod.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21use crate::plugins::inspector::editors::resource::{ResourceFieldBuilder, ResourceFieldMessage};
22use crate::{
23    audio::bus::{AudioBusView, AudioBusViewBuilder, AudioBusViewMessage},
24    command::CommandGroup,
25    fyrox::{
26        core::pool::Handle,
27        engine::Engine,
28        graph::BaseSceneGraph,
29        gui::{
30            button::{ButtonBuilder, ButtonMessage},
31            dropdown_list::{DropdownListBuilder, DropdownListMessage},
32            grid::{Column, Row},
33            list_view::{ListView, ListViewBuilder, ListViewMessage},
34            message::UiMessage,
35            stack_panel::StackPanelBuilder,
36            text::TextBuilder,
37            utils::make_simple_tooltip,
38            widget::{WidgetBuilder, WidgetMessage},
39            window::{WindowBuilder, WindowTitle},
40            Orientation, Thickness, UiNode, VerticalAlignment,
41        },
42        scene::sound::{AudioBus, AudioBusGraph, DistanceModel, HrirSphereResourceData, Renderer},
43    },
44    message::MessageSender,
45    scene::{
46        commands::{
47            effect::{AddAudioBusCommand, LinkAudioBuses, RemoveAudioBusCommand},
48            sound_context::{
49                SetDistanceModelCommand, SetHrtfRendererHrirSphereResource, SetRendererCommand,
50            },
51        },
52        SelectionContainer,
53    },
54    send_sync_message,
55    utils::window_content,
56    ChangeSelectionCommand, Command, GameScene, GridBuilder, MessageDirection, Mode, Selection,
57    UserInterface,
58};
59use fyrox::gui::utils::make_dropdown_list_option;
60use std::cmp::Ordering;
61use strum::VariantNames;
62
63mod bus;
64pub mod preview;
65
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct AudioBusSelection {
68    pub buses: Vec<Handle<AudioBus>>,
69}
70
71impl SelectionContainer for AudioBusSelection {
72    fn len(&self) -> usize {
73        self.buses.len()
74    }
75}
76
77pub struct AudioPanel {
78    pub window: Handle<UiNode>,
79    add_bus: Handle<UiNode>,
80    remove_bus: Handle<UiNode>,
81    audio_buses: Handle<UiNode>,
82    distance_model: Handle<UiNode>,
83    renderer: Handle<UiNode>,
84    hrir_resource: Handle<UiNode>,
85}
86
87fn item_bus(item: Handle<UiNode>, ui: &UserInterface) -> Handle<AudioBus> {
88    ui.node(item).query_component::<AudioBusView>().unwrap().bus
89}
90
91fn fetch_possible_parent_buses(
92    bus: Handle<AudioBus>,
93    graph: &AudioBusGraph,
94) -> Vec<(Handle<AudioBus>, String)> {
95    let mut stack = vec![graph.primary_bus_handle()];
96    let mut result = Vec::new();
97    while let Some(other_bus) = stack.pop() {
98        let other_bus_ref = graph.try_get_bus_ref(other_bus).expect("Malformed graph!");
99        if other_bus != bus {
100            result.push((other_bus, other_bus_ref.name().to_owned()));
101            stack.extend_from_slice(other_bus_ref.children());
102        }
103    }
104    result
105}
106
107fn audio_bus_effect_names(audio_bus: &AudioBus) -> Vec<String> {
108    audio_bus
109        .effects()
110        .map(|e| AsRef::<str>::as_ref(e).to_owned())
111        .collect::<Vec<_>>()
112}
113
114impl AudioPanel {
115    pub fn new(engine: &mut Engine, sender: MessageSender) -> Self {
116        let ctx = &mut engine.user_interfaces.first_mut().build_ctx();
117
118        let add_bus;
119        let remove_bus;
120        let buses;
121        let distance_model;
122        let renderer;
123        let hrir_resource;
124        let window = WindowBuilder::new(WidgetBuilder::new().with_name("AudioPanel"))
125            .with_content(
126                GridBuilder::new(
127                    WidgetBuilder::new()
128                        .with_child(
129                            StackPanelBuilder::new(
130                                WidgetBuilder::new()
131                                    .on_row(0)
132                                    .with_child(
133                                        TextBuilder::new(
134                                            WidgetBuilder::new()
135                                                .with_margin(Thickness::uniform(1.0)),
136                                        )
137                                        .with_vertical_text_alignment(VerticalAlignment::Center)
138                                        .with_text("DM")
139                                        .build(ctx),
140                                    )
141                                    .with_child({
142                                        distance_model = DropdownListBuilder::new(
143                                            WidgetBuilder::new()
144                                                .with_tab_index(Some(0))
145                                                .with_margin(Thickness::uniform(1.0))
146                                                .with_width(130.0)
147                                                .with_tooltip(make_simple_tooltip(
148                                                    ctx,
149                                                    "Distance Model. Defines the method of \
150                                                    calculating distance attenuation for sound \
151                                                    sources.",
152                                                )),
153                                        )
154                                        .with_items(
155                                            DistanceModel::VARIANTS
156                                                .iter()
157                                                .map(|v| make_dropdown_list_option(ctx, v))
158                                                .collect::<Vec<_>>(),
159                                        )
160                                        .build(ctx);
161                                        distance_model
162                                    })
163                                    .with_child(
164                                        TextBuilder::new(
165                                            WidgetBuilder::new()
166                                                .with_margin(Thickness::uniform(1.0)),
167                                        )
168                                        .with_vertical_text_alignment(VerticalAlignment::Center)
169                                        .with_text("Renderer")
170                                        .build(ctx),
171                                    )
172                                    .with_child({
173                                        renderer = DropdownListBuilder::new(
174                                            WidgetBuilder::new()
175                                                .with_tab_index(Some(1))
176                                                .with_margin(Thickness::uniform(1.0))
177                                                .with_width(100.0)
178                                                .with_tooltip(make_simple_tooltip(ctx, "Renderer")),
179                                        )
180                                        .with_items(
181                                            Renderer::VARIANTS
182                                                .iter()
183                                                .map(|v| make_dropdown_list_option(ctx, v))
184                                                .collect::<Vec<_>>(),
185                                        )
186                                        .build(ctx);
187                                        renderer
188                                    })
189                                    .with_child({
190                                        hrir_resource =
191                                            ResourceFieldBuilder::<HrirSphereResourceData>::new(
192                                                WidgetBuilder::new().with_tab_index(Some(2)),
193                                                sender,
194                                            )
195                                            .build(ctx, engine.resource_manager.clone());
196                                        hrir_resource
197                                    }),
198                            )
199                            .with_orientation(Orientation::Horizontal)
200                            .build(ctx),
201                        )
202                        .with_child({
203                            buses = ListViewBuilder::new(
204                                WidgetBuilder::new().on_row(1).with_tab_index(Some(3)),
205                            )
206                            .with_items_panel(
207                                StackPanelBuilder::new(WidgetBuilder::new())
208                                    .with_orientation(Orientation::Horizontal)
209                                    .build(ctx),
210                            )
211                            .build(ctx);
212                            buses
213                        })
214                        .with_child(
215                            StackPanelBuilder::new(
216                                WidgetBuilder::new()
217                                    .on_row(2)
218                                    .with_child({
219                                        add_bus = ButtonBuilder::new(
220                                            WidgetBuilder::new()
221                                                .with_tab_index(Some(4))
222                                                .with_width(100.0)
223                                                .with_margin(Thickness::uniform(1.0)),
224                                        )
225                                        .with_text("Add Bus")
226                                        .build(ctx);
227                                        add_bus
228                                    })
229                                    .with_child({
230                                        remove_bus = ButtonBuilder::new(
231                                            WidgetBuilder::new()
232                                                .with_tab_index(Some(5))
233                                                .with_width(100.0)
234                                                .with_enabled(false)
235                                                .with_margin(Thickness::uniform(1.0)),
236                                        )
237                                        .with_text("Remove Bus")
238                                        .build(ctx);
239                                        remove_bus
240                                    }),
241                            )
242                            .with_orientation(Orientation::Horizontal)
243                            .build(ctx),
244                        ),
245                )
246                .add_column(Column::stretch())
247                .add_row(Row::strict(25.0))
248                .add_row(Row::stretch())
249                .add_row(Row::strict(25.0))
250                .build(ctx),
251            )
252            .with_title(WindowTitle::text("Audio Context"))
253            .build(ctx);
254
255        Self {
256            window,
257            audio_buses: buses,
258            distance_model,
259            add_bus,
260            remove_bus,
261            renderer,
262            hrir_resource,
263        }
264    }
265
266    pub fn handle_ui_message(
267        &mut self,
268        message: &UiMessage,
269        editor_selection: &Selection,
270        sender: &MessageSender,
271        engine: &Engine,
272    ) {
273        if let Some(ButtonMessage::Click) = message.data() {
274            if message.destination() == self.add_bus {
275                sender.do_command(AddAudioBusCommand::new(AudioBus::new(
276                    "AudioBus".to_string(),
277                )))
278            } else if message.destination() == self.remove_bus {
279                if let Some(selection) = editor_selection.as_audio_bus() {
280                    let mut commands = vec![Command::new(ChangeSelectionCommand::new(
281                        Selection::new_empty(),
282                    ))];
283
284                    for &bus in &selection.buses {
285                        commands.push(Command::new(RemoveAudioBusCommand::new(bus)));
286                    }
287
288                    sender.do_command(CommandGroup::from(commands));
289                }
290            }
291        } else if let Some(ListViewMessage::SelectionChanged(selected_indices)) = message.data() {
292            if message.destination() == self.audio_buses
293                && message.direction() == MessageDirection::FromWidget
294            {
295                let ui = &engine.user_interfaces.first();
296
297                let mut selection = Vec::new();
298
299                for bus_index in selected_indices {
300                    let bus = item_bus(
301                        ui.node(self.audio_buses)
302                            .cast::<ListView>()
303                            .expect("Must be ListView")
304                            .items()[*bus_index],
305                        ui,
306                    );
307
308                    selection.push(bus);
309                }
310
311                sender.do_command(ChangeSelectionCommand::new(Selection::new(
312                    AudioBusSelection { buses: selection },
313                )))
314            }
315        } else if let Some(AudioBusViewMessage::ChangeParent(new_parent)) = message.data() {
316            if message.direction() == MessageDirection::FromWidget {
317                let audio_bus_view_ref = engine
318                    .user_interfaces
319                    .first()
320                    .node(message.destination())
321                    .query_component::<AudioBusView>()
322                    .unwrap();
323
324                let child = audio_bus_view_ref.bus;
325
326                sender.do_command(LinkAudioBuses {
327                    child,
328                    parent: *new_parent,
329                });
330            }
331        } else if let Some(DropdownListMessage::SelectionChanged(Some(index))) = message.data() {
332            if message.direction() == MessageDirection::FromWidget {
333                if message.destination() == self.renderer {
334                    let renderer = match index {
335                        0 => Renderer::Default,
336                        1 => Renderer::HrtfRenderer(Default::default()),
337                        _ => unreachable!(),
338                    };
339
340                    sender.do_command(SetRendererCommand::new(renderer));
341                } else if message.destination() == self.distance_model {
342                    let distance_model = match index {
343                        0 => DistanceModel::None,
344                        1 => DistanceModel::InverseDistance,
345                        2 => DistanceModel::LinearDistance,
346                        3 => DistanceModel::ExponentDistance,
347                        _ => unreachable!(),
348                    };
349
350                    sender.do_command(SetDistanceModelCommand::new(distance_model));
351                }
352            }
353        } else if let Some(ResourceFieldMessage::Value(resource)) =
354            message.data::<ResourceFieldMessage<HrirSphereResourceData>>()
355        {
356            if message.destination() == self.hrir_resource
357                && message.direction() == MessageDirection::FromWidget
358            {
359                sender.do_command(SetHrtfRendererHrirSphereResource::new(resource.clone()));
360            }
361        }
362    }
363
364    pub fn sync_to_model(
365        &mut self,
366        editor_selection: &Selection,
367        game_scene: &GameScene,
368        engine: &mut Engine,
369    ) {
370        let context_state = engine.scenes[game_scene.scene].graph.sound_context.state();
371        let ui = &mut engine.user_interfaces.first_mut();
372
373        let items = ui
374            .node(self.audio_buses)
375            .cast::<ListView>()
376            .expect("Must be ListView!")
377            .items()
378            .to_vec();
379
380        match (context_state.bus_graph_ref().len()).cmp(&items.len()) {
381            Ordering::Less => {
382                for &item in &items {
383                    let bus_handle = item_bus(item, ui);
384                    if context_state
385                        .bus_graph_ref()
386                        .buses_pair_iter()
387                        .all(|(other_bus_handle, _)| other_bus_handle != bus_handle)
388                    {
389                        send_sync_message(
390                            ui,
391                            ListViewMessage::remove_item(
392                                self.audio_buses,
393                                MessageDirection::ToWidget,
394                                item,
395                            ),
396                        );
397                    }
398                }
399            }
400            Ordering::Greater => {
401                for (audio_bus_handle, audio_bus) in context_state.bus_graph_ref().buses_pair_iter()
402                {
403                    if items.iter().all(|i| item_bus(*i, ui) != audio_bus_handle) {
404                        let item = AudioBusViewBuilder::new(
405                            WidgetBuilder::new()
406                                .with_width(100.0)
407                                .with_margin(Thickness::uniform(1.0)),
408                        )
409                        .with_name(audio_bus.name())
410                        .with_effect_names(audio_bus_effect_names(audio_bus))
411                        .with_parent_bus(audio_bus.parent())
412                        .with_possible_parent_buses(fetch_possible_parent_buses(
413                            audio_bus_handle,
414                            context_state.bus_graph_ref(),
415                        ))
416                        .with_audio_bus(audio_bus_handle)
417                        .build(&mut ui.build_ctx());
418
419                        send_sync_message(
420                            ui,
421                            ListViewMessage::add_item(
422                                self.audio_buses,
423                                MessageDirection::ToWidget,
424                                item,
425                            ),
426                        );
427                    }
428                }
429            }
430            _ => (),
431        }
432
433        let mut selected_buses = Vec::new();
434        let mut is_primary_bus_selected = false;
435
436        if let Some(selection) = editor_selection.as_audio_bus() {
437            for (index, item) in items.into_iter().enumerate() {
438                let bus_handle = item_bus(item, ui);
439
440                if selection.buses.contains(&bus_handle) {
441                    selected_buses.push(index);
442
443                    if context_state.bus_graph_ref().primary_bus_handle() == bus_handle {
444                        is_primary_bus_selected = true;
445                    }
446
447                    break;
448                }
449            }
450        }
451
452        send_sync_message(
453            ui,
454            WidgetMessage::enabled(
455                self.remove_bus,
456                MessageDirection::ToWidget,
457                !selected_buses.is_empty() && !is_primary_bus_selected,
458            ),
459        );
460
461        send_sync_message(
462            ui,
463            ListViewMessage::selection(
464                self.audio_buses,
465                MessageDirection::ToWidget,
466                selected_buses,
467            ),
468        );
469
470        for audio_bus_view in ui
471            .node(self.audio_buses)
472            .cast::<ListView>()
473            .expect("Must be ListView!")
474            .items()
475        {
476            let audio_bus_view_ref = ui
477                .node(*audio_bus_view)
478                .query_component::<AudioBusView>()
479                .unwrap();
480            send_sync_message(
481                ui,
482                AudioBusViewMessage::possible_parent_buses(
483                    *audio_bus_view,
484                    MessageDirection::ToWidget,
485                    fetch_possible_parent_buses(
486                        audio_bus_view_ref.bus,
487                        context_state.bus_graph_ref(),
488                    ),
489                ),
490            );
491            if let Some(audio_bus_ref) = context_state
492                .bus_graph_ref()
493                .try_get_bus_ref(audio_bus_view_ref.bus)
494            {
495                send_sync_message(
496                    ui,
497                    AudioBusViewMessage::effect_names(
498                        *audio_bus_view,
499                        MessageDirection::ToWidget,
500                        audio_bus_effect_names(audio_bus_ref),
501                    ),
502                );
503                send_sync_message(
504                    ui,
505                    AudioBusViewMessage::name(
506                        *audio_bus_view,
507                        MessageDirection::ToWidget,
508                        audio_bus_ref.name().to_owned(),
509                    ),
510                );
511            }
512        }
513
514        send_sync_message(
515            ui,
516            DropdownListMessage::selection(
517                self.distance_model,
518                MessageDirection::ToWidget,
519                Some(context_state.distance_model() as usize),
520            ),
521        );
522
523        send_sync_message(
524            ui,
525            DropdownListMessage::selection(
526                self.renderer,
527                MessageDirection::ToWidget,
528                Some(match context_state.renderer_ref() {
529                    Renderer::Default => 0,
530                    Renderer::HrtfRenderer(_) => 1,
531                }),
532            ),
533        );
534
535        if let Renderer::HrtfRenderer(hrtf) = context_state.renderer_ref() {
536            send_sync_message(
537                ui,
538                WidgetMessage::visibility(self.hrir_resource, MessageDirection::ToWidget, true),
539            );
540
541            send_sync_message(
542                ui,
543                ResourceFieldMessage::value(
544                    self.hrir_resource,
545                    MessageDirection::ToWidget,
546                    hrtf.hrir_sphere_resource(),
547                ),
548            );
549        } else {
550            send_sync_message(
551                ui,
552                WidgetMessage::visibility(self.hrir_resource, MessageDirection::ToWidget, false),
553            );
554        }
555    }
556
557    pub fn on_mode_changed(&mut self, ui: &UserInterface, mode: &Mode) {
558        ui.send_message(WidgetMessage::enabled(
559            window_content(self.window, ui),
560            MessageDirection::ToWidget,
561            mode.is_edit(),
562        ));
563    }
564}