Skip to content

Commit

Permalink
Add scene audio editor (#980)
Browse files Browse the repository at this point in the history
  • Loading branch information
bigfoodK authored Nov 1, 2024
1 parent 6a407bf commit 4b91c77
Show file tree
Hide file tree
Showing 9 changed files with 549 additions and 3 deletions.
84 changes: 84 additions & 0 deletions luda-editor/new-client/src/audio_util/audio_storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use namui::*;
use std::{
collections::HashMap,
error::Error,
sync::{Arc, RwLock},
};

lazy_static! {
static ref AUDIO_STORAGE: AudioStorage = AudioStorage::new();
}

pub fn get_or_load_audio(audio_id: String) -> Arc<AudioLoadState> {
let Some(load_state) = AUDIO_STORAGE.get(&audio_id) else {
let loading = AUDIO_STORAGE.set(audio_id.clone(), AudioLoadState::Loading);
spawn(async move {
let audio_id = audio_id.clone();

let request = match network::http::Request::get(audio_url(&audio_id)).body(()) {
Ok(response) => response,
Err(error) => {
AUDIO_STORAGE.set(audio_id, AudioLoadState::Error(error.into()));
return;
}
};
let bytes = match request.send().await {
Ok(response) => response.bytes(),
Err(error) => {
AUDIO_STORAGE.set(audio_id, AudioLoadState::Error(error.into()));
return;
}
};
let bytes = match bytes.await {
Ok(bytes) => bytes,
Err(error) => {
AUDIO_STORAGE.set(audio_id, AudioLoadState::Error(error.into()));
return;
}
};
let load_state = match Audio::from_ogg_opus_bytes(bytes) {
Ok(audio) => AudioLoadState::Loaded { audio },
Err(error) => AudioLoadState::Error(error.into()),
};

AUDIO_STORAGE.set(audio_id, load_state);
});
return loading;
};
load_state
}

pub enum AudioLoadState {
Loading,
Loaded {
audio: Audio,
},
#[allow(unused)]
Error(Box<dyn Error + Send + Sync>),
}
struct AudioStorage {
storage: RwLock<HashMap<String, Arc<AudioLoadState>>>,
}
impl AudioStorage {
fn new() -> Self {
Self {
storage: RwLock::new(HashMap::new()),
}
}
fn get(&self, sprite_id: &str) -> Option<Arc<AudioLoadState>> {
self.storage.read().unwrap().get(sprite_id).cloned()
}
fn set(&self, sprite_id: String, load_state: AudioLoadState) -> Arc<AudioLoadState> {
let load_state = Arc::new(load_state);
self.storage
.write()
.unwrap()
.insert(sprite_id, load_state.clone());
load_state
}
}

fn audio_url(audio_id: &str) -> String {
const PREFIX: &str = "http://localhost:4566/visual-novel-asset/audio/after-transcode";
format!("{PREFIX}/{audio_id}")
}
3 changes: 3 additions & 0 deletions luda-editor/new-client/src/audio_util/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod audio_storage;

pub use audio_storage::*;
1 change: 1 addition & 0 deletions luda-editor/new-client/src/episode_editor/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod properties_panel;
mod scene_audio_editor;
mod scene_list;
mod scene_preview;
mod scene_sprite_editor;
Expand Down
11 changes: 9 additions & 2 deletions luda-editor/new-client/src/episode_editor/properties_panel.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::scene_sprite_editor::SceneSpriteEditor;
use super::{scene_audio_editor::SceneAudioEditor, scene_sprite_editor::SceneSpriteEditor};
use luda_rpc::{AssetDoc, EpisodeEditAction, Scene};
use namui::*;
use namui_prebuilt::{button, table::*};
Expand Down Expand Up @@ -66,7 +66,14 @@ impl Component for PropertiesPanel<'_> {
});
}
PropertiesPanelTab::Background => {}
PropertiesPanelTab::Audio => {}
PropertiesPanelTab::Audio => {
ctx.add(SceneAudioEditor {
wh,
scene,
update_scene,
asset_docs,
});
}
}),
])(wh, ctx);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
use crate::*;
use audio_util::{get_or_load_audio, AudioLoadState};
use list_view::AutoListView;
use luda_rpc::*;
use std::collections::{HashMap, HashSet};
use time::now;

pub struct AudioSelectTool<'a> {
pub wh: Wh<Px>,
pub asset_docs: Sig<'a, HashMap<String, AssetDoc>>,
pub selected_audio: &'a Option<SceneSound>,
pub set_audio: &'a dyn Fn(Option<SceneSound>),
}

impl Component for AudioSelectTool<'_> {
fn render(self, ctx: &RenderCtx) {
let Self {
wh,
asset_docs,
selected_audio,
set_audio,
} = self;

let (selected_tags, set_selected_tags) =
ctx.state::<HashSet<AssetSystemTag>>(Default::default);

let on_select = |audio_id: Option<String>| {
let audio = audio_id.map(|audio_id| SceneSound {
sound_id: audio_id,
volume: selected_audio
.as_ref()
.map(|selected_audio| selected_audio.volume)
.unwrap_or(100.percent()),
});
set_audio(audio);
};

let tag_filtered_asset_docs = ctx.memo(|| {
asset_docs
.iter()
.filter(|(_id, asset_tag)| {
if !matches!(asset_tag.asset_kind, AssetKind::Audio) {
return false;
}
asset_tag.tags.iter().any(|tag| match tag {
AssetTag::System { tag } => selected_tags.contains(tag),
AssetTag::Custom { .. } => false,
})
})
.map(|(id, audio)| (id.clone(), audio.clone()))
.collect::<HashMap<String, AssetDoc>>()
});

let tag_toggle_button = |tag: AssetSystemTag| {
let is_on = selected_tags.contains(&tag);
let text = match tag {
AssetSystemTag::AudioCharacter => "인물",
AssetSystemTag::AudioProp => "사물",
AssetSystemTag::AudioBackground => "배경",
_ => unreachable!(),
};

table::ratio(1, move |wh, ctx| {
ctx.add(simple_toggle_button(wh, text, is_on, |_| {
set_selected_tags.mutate(move |selected_tags| {
if selected_tags.contains(&tag) {
selected_tags.remove(&tag);
} else {
selected_tags.insert(tag);
}
});
}));
})
};

ctx.compose(|ctx| {
table::vertical([
table::fixed(
64.px(),
table::horizontal([
table::fixed(64.px(), |_, _| {}),
tag_toggle_button(AssetSystemTag::AudioCharacter),
table::fixed(16.px(), |_, _| {}),
tag_toggle_button(AssetSystemTag::AudioProp),
table::fixed(16.px(), |_, _| {}),
tag_toggle_button(AssetSystemTag::AudioBackground),
table::fixed(64.px(), |_, _| {}),
]),
),
table::ratio(1, |wh, ctx| {
ctx.add(AudioList {
wh,
asset_docs: tag_filtered_asset_docs,
selected_audio,
on_select: &on_select,
});
}),
])(wh, ctx)
});
}
}

struct AudioList<'a> {
wh: Wh<Px>,
asset_docs: Sig<'a, HashMap<String, AssetDoc>>,
selected_audio: &'a Option<SceneSound>,
on_select: &'a dyn Fn(Option<String>),
}
impl Component for AudioList<'_> {
fn render(self, ctx: &RenderCtx) {
let Self {
wh,
asset_docs,
selected_audio,
on_select,
} = self;

let item_wh = Wh::new(wh.width, 48.px());
let render_item = |text: String, audio_id: Option<String>| {
let is_on = selected_audio
.as_ref()
.map(|selected_audio| &selected_audio.sound_id)
.eq(&audio_id.as_ref());

(
audio_id.clone().unwrap_or_default(),
AudioListItem {
wh: item_wh,
audio_id,
text,
is_on,
on_select,
},
)
};

let mut items = vec![render_item("없음".to_string(), None)];
items.extend(asset_docs.values().filter_map(|asset_doc| {
let AssetKind::Audio = asset_doc.asset_kind else {
return None;
};
Some(render_item(
asset_doc.name.to_string(),
Some(asset_doc.id.clone()),
))
}));

ctx.add(AutoListView {
height: wh.height,
scroll_bar_width: 10.px(),
item_wh,
items: items.into_iter(),
});
}
}

struct AudioListItem<'a> {
wh: Wh<Px>,
audio_id: Option<String>,
text: String,
is_on: bool,
on_select: &'a dyn Fn(Option<String>),
}
impl Component for AudioListItem<'_> {
fn render(self, ctx: &RenderCtx) {
let Self {
wh,
audio_id,
text,
is_on,
on_select,
} = self;

let audio = audio_id.clone().map(get_or_load_audio);
let (hovering, set_hovering) = ctx.state::<Option<Hovering>>(|| None);
let (play_handle, set_play_handle) = ctx.state(|| None);

ctx.interval("play audio if hovering", 1.sec(), |_| {
let Some((Hovering { started_at }, audio)) =
hovering.as_ref().as_ref().zip(audio.as_ref())
else {
return;
};
if play_handle.is_some() {
return;
}
if now() - started_at < 1.sec() {
return;
}
let AudioLoadState::Loaded { audio } = audio.as_ref() else {
return;
};
let play_handle = audio.play_repeat();
set_play_handle.set(Some(play_handle));
});

ctx.add(
simple_toggle_button(wh, text, is_on, |_| {
on_select(audio_id);
})
.attach_event(|event| {
let Event::MouseMove { event } = event else {
return;
};
match hovering.is_some() {
true => {
if event.is_local_xy_in() {
return;
}
set_hovering.set(None);
set_play_handle.set(None);
}
false => {
if !event.is_local_xy_in() {
return;
}
set_hovering.set(Some(Hovering { started_at: now() }));
}
}
}),
);
}
}
struct Hovering {
started_at: Instant,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
mod audio_select_tool;
mod volume_tool;

use luda_rpc::{AssetDoc, Scene, SceneSound};
use namui::*;
use namui_prebuilt::*;
use std::collections::HashMap;

pub struct SceneAudioEditor<'a> {
pub wh: Wh<Px>,
pub scene: &'a Scene,
pub update_scene: &'a dyn Fn(Scene),
pub asset_docs: Sig<'a, HashMap<String, AssetDoc>>,
}

impl Component for SceneAudioEditor<'_> {
fn render(self, ctx: &RenderCtx) {
let Self {
wh,
scene,
update_scene,
asset_docs,
} = self;

let set_audio = |audio: Option<SceneSound>| {
let mut scene = scene.clone();
scene.bgm = audio;
update_scene(scene);
};

ctx.compose(|ctx| {
table::vertical([
table::fixed(64.px(), |wh, ctx| {
ctx.add(volume_tool::VolumeTool {
wh,
selected_audio: &scene.bgm,
set_audio: &set_audio,
});
}),
table::ratio(1, |wh, ctx| {
ctx.add(audio_select_tool::AudioSelectTool {
wh,
asset_docs: asset_docs.clone(),
selected_audio: &scene.bgm,
set_audio: &set_audio,
});
}),
])(wh, ctx)
});
}
}
Loading

0 comments on commit 4b91c77

Please sign in to comment.