Skip to main content

ass_editor/commands/karaoke_commands/
apply_impl.rs

1//! Execution and template-application logic for [`ApplyKaraokeCommand`].
2
3use super::{ApplyKaraokeCommand, KaraokeTemplate, KaraokeType};
4use crate::commands::{CommandResult, EditorCommand};
5use crate::core::{EditorDocument, Position, Range, Result};
6
7#[cfg(not(feature = "std"))]
8use alloc::{
9    format,
10    string::{String, ToString},
11    vec,
12    vec::Vec,
13};
14
15impl EditorCommand for ApplyKaraokeCommand {
16    fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
17        let original_text = document.text_range(self.event_range)?;
18        let karaoke_text = self.apply_karaoke_template(&original_text, document)?;
19
20        document.replace_raw(self.event_range, &karaoke_text)?;
21
22        let end_pos = Position::new(self.event_range.start.offset + karaoke_text.len());
23        let range = Range::new(self.event_range.start, end_pos);
24
25        Ok(CommandResult::success_with_change(range, end_pos))
26    }
27
28    fn description(&self) -> &str {
29        match &self.karaoke_template {
30            KaraokeTemplate::Equal { .. } => "Apply equal karaoke timing",
31            KaraokeTemplate::Beat { .. } => "Apply beat-based karaoke timing",
32            KaraokeTemplate::Pattern { .. } => "Apply pattern-based karaoke timing",
33            KaraokeTemplate::ImportFrom { .. } => "Import karaoke timing",
34        }
35    }
36
37    fn memory_usage(&self) -> usize {
38        let template_size = match &self.karaoke_template {
39            KaraokeTemplate::Pattern { durations, .. } => {
40                durations.len() * core::mem::size_of::<u32>()
41            }
42            _ => 0,
43        };
44        core::mem::size_of::<Self>() + template_size
45    }
46}
47
48impl ApplyKaraokeCommand {
49    /// Apply karaoke template to text
50    fn apply_karaoke_template(&self, text: &str, _document: &EditorDocument) -> Result<String> {
51        // Extract text content from event (skip override blocks for syllable detection)
52        let clean_text = self.extract_clean_text(text);
53        let syllables = self.detect_syllables(&clean_text);
54
55        match &self.karaoke_template {
56            KaraokeTemplate::Equal {
57                syllable_duration,
58                karaoke_type,
59            } => self.apply_equal_timing(&syllables, *syllable_duration, *karaoke_type),
60            KaraokeTemplate::Beat {
61                beats_per_minute,
62                beats_per_syllable,
63                karaoke_type,
64            } => self.apply_beat_timing(
65                &syllables,
66                *beats_per_minute,
67                *beats_per_syllable,
68                *karaoke_type,
69            ),
70            KaraokeTemplate::Pattern {
71                durations,
72                karaoke_type,
73            } => self.apply_pattern_timing(&syllables, durations, *karaoke_type),
74            KaraokeTemplate::ImportFrom {
75                source_event_index: _,
76            } => {
77                // Simplified - would need to parse other events
78                Ok(text.to_string())
79            }
80        }
81    }
82
83    /// Extract clean text without override blocks
84    fn extract_clean_text(&self, text: &str) -> String {
85        let mut result = String::new();
86        let mut chars = text.chars();
87
88        while let Some(ch) = chars.next() {
89            if ch == '{' {
90                // Skip override block
91                let mut brace_count = 1;
92                for inner_ch in chars.by_ref() {
93                    if inner_ch == '{' {
94                        brace_count += 1;
95                    } else if inner_ch == '}' {
96                        brace_count -= 1;
97                        if brace_count == 0 {
98                            break;
99                        }
100                    }
101                }
102            } else {
103                result.push(ch);
104            }
105        }
106
107        result
108    }
109
110    /// Detect syllables in clean text
111    fn detect_syllables(&self, text: &str) -> Vec<String> {
112        // Simple syllable detection - split on spaces and vowel boundaries
113        text.split_whitespace()
114            .flat_map(|word| {
115                // For now, treat each word as one syllable
116                // In practice, you'd want more sophisticated syllable detection
117                vec![word.to_string()]
118            })
119            .collect()
120    }
121
122    /// Apply equal timing to syllables
123    fn apply_equal_timing(
124        &self,
125        syllables: &[String],
126        duration: u32,
127        karaoke_type: KaraokeType,
128    ) -> Result<String> {
129        let tag = karaoke_type.tag_string();
130        let mut result = String::new();
131
132        for (i, syllable) in syllables.iter().enumerate() {
133            if i > 0 {
134                result.push(' '); // Add space between syllables
135            }
136            result.push_str(&format!("{{\\{tag}{duration}}}{syllable}"));
137        }
138
139        Ok(result)
140    }
141
142    /// Apply beat-based timing to syllables
143    fn apply_beat_timing(
144        &self,
145        syllables: &[String],
146        bpm: u32,
147        beats_per_syllable: f32,
148        karaoke_type: KaraokeType,
149    ) -> Result<String> {
150        // Calculate duration in centiseconds: (60 seconds / BPM) * beats_per_syllable * 100 cs/s
151        let duration = ((60.0 / bpm as f32) * beats_per_syllable * 100.0) as u32;
152        self.apply_equal_timing(syllables, duration, karaoke_type)
153    }
154
155    /// Apply pattern-based timing to syllables
156    fn apply_pattern_timing(
157        &self,
158        syllables: &[String],
159        durations: &[u32],
160        karaoke_type: KaraokeType,
161    ) -> Result<String> {
162        let tag = karaoke_type.tag_string();
163        let mut result = String::new();
164
165        for (i, syllable) in syllables.iter().enumerate() {
166            if i > 0 {
167                result.push(' ');
168            }
169            let duration = durations.get(i % durations.len()).copied().unwrap_or(50);
170            result.push_str(&format!("{{\\{tag}{duration}}}{syllable}"));
171        }
172
173        Ok(result)
174    }
175}