From 489aff2540eee57a34f6f45554fb1de6abe92d45 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 1 Sep 2016 01:56:38 -0400 Subject: [PATCH] Ability to change speed + UI; also refactor volume - Speed is an effect that stretches the waveform. - Volume is now an effect. - Poor UI added for speed component. --- src/effects/mod.rs | 5 ++++ src/effects/speed.rs | 28 +++++++++++++++++++ src/effects/volume.rs | 35 +++++++++++++++++++++++ src/handlers/audio_synth_handler.rs | 38 +++++++++++++++++++++++-- src/main.rs | 1 + src/synthesizer.rs | 43 ++++++++--------------------- src/web/js/events.js | 4 ++- src/web/js/initialize.js | 2 ++ src/web/js/url.js | 20 ++++++++++++-- web/index.html | 6 ++++ 10 files changed, 144 insertions(+), 38 deletions(-) create mode 100644 src/effects/mod.rs create mode 100644 src/effects/speed.rs create mode 100644 src/effects/volume.rs diff --git a/src/effects/mod.rs b/src/effects/mod.rs new file mode 100644 index 0000000000..1c09e5cf5d --- /dev/null +++ b/src/effects/mod.rs @@ -0,0 +1,5 @@ +// Copyright (c) 2016 Brandon Thomas + +pub mod speed; +pub mod volume; + diff --git a/src/effects/speed.rs b/src/effects/speed.rs new file mode 100644 index 0000000000..445b854588 --- /dev/null +++ b/src/effects/speed.rs @@ -0,0 +1,28 @@ +// Copyright (c) 2016 Brandon Thomas + +/** + * Change the speed of a waveform, producing a waveform that is + * "slower" or "faster". A speed of "0.5" will be slower (thus + * longer), and a speed of "2.0" will be faster (thus shorter). + */ +pub fn change_speed(waveform: Vec, speed: f32) -> Vec { + let change = 1.0 / speed; + let mut size = (waveform.len() as f32 * change).round() as usize; + + // FIXME: Use memory of caller. + // Caller can be more efficient with memory management. + let mut stretched : Vec = Vec::with_capacity(size); + + for i in 0..size { + let j = (i as f32 / change).floor() as usize; + stretched.push(waveform[j]); + } + + // Heuristic: odd sized waveforms don't seem to work. (Due to # channels?) + if size % 2 == 1 { + stretched.push(0); + } + + stretched +} + diff --git a/src/effects/volume.rs b/src/effects/volume.rs new file mode 100644 index 0000000000..1c82d2361b --- /dev/null +++ b/src/effects/volume.rs @@ -0,0 +1,35 @@ +// Copyright (c) 2016 Brandon Thomas + +use std::i16; + +/** + * Change the volume of a waveform. + * TODO: Doc. + */ +pub fn change_volume(waveform: Vec, volume: f32) -> Vec { + let mut transformed : Vec = Vec::with_capacity(waveform.len()); + for x in &waveform { + let data = raise_volume(*x, volume); + transformed.push(data); + } + + transformed +} + +/// Raise the volume of a sample by changing its amplitude. +fn raise_volume(data: i16, vol: f32) -> i16 { + // TODO: Cleanup, make more efficient. + let f : f32 = data as f32 * vol; + let g = f as i32; + + let h : i16 = if g > i16::MAX as i32 { + i16::MAX + } else if g < i16::MIN as i32 { + i16::MIN + } else { + g as i16 + }; + + h +} + diff --git a/src/handlers/audio_synth_handler.rs b/src/handlers/audio_synth_handler.rs index 3969eff19a..0f22ef59de 100644 --- a/src/handlers/audio_synth_handler.rs +++ b/src/handlers/audio_synth_handler.rs @@ -33,11 +33,13 @@ use words::split_sentence; const SENTENCE_PARAM : &'static str = "s"; const SPEAKER_PARAM : &'static str = "v"; +const SPEED_PARAM : &'static str = "spd"; const USE_PHONEMES_PARAM: &'static str = "up"; const USE_WORDS_PARAM: &'static str = "uw"; const VOLUME_PARAM : &'static str = "vol"; /// Represents a request to this endpoint. +#[derive(Debug)] struct SpeakRequest { /** The sentence to be spoken. */ pub sentence: String, @@ -48,6 +50,9 @@ struct SpeakRequest { /** An optional volume multiplier. */ pub volume: Option, + /** An optional speed multiplier. */ + pub speed: Option, + /** Whether to use phonemes. */ pub use_phonemes: bool, @@ -122,6 +127,28 @@ impl SpeakRequest { }, }; + let speed : Option = match map.get(SPEED_PARAM) { + None => { None }, + Some(list) => { + match list.get(0) { + None => { None }, + Some(s) => { + match s.trim().parse::() { + Err(_) => None, + Ok(i) => { + let diff = i - 1.0; + if diff < 0.005 && diff > -0.005 { + None // Don't waste CPU calculating. + } else { + Some(i) + } + } + } + }, + } + }, + }; + let use_phonemes = match map.get(USE_PHONEMES_PARAM) { None => { true }, Some(list) => { @@ -156,6 +183,7 @@ impl SpeakRequest { sentence: sen, speaker: spk, volume: volume, + speed: speed, use_phonemes: use_phonemes, use_words: use_words, }) @@ -192,6 +220,7 @@ impl Handler for AudioSynthHandler { }; info!("Speak Request ({}): {}.", request.speaker, request.sentence); + info!("Request: {:?}", request); // FIXME: Varies with spaces, formatting, etc. let hash = self.sha_digest(&request); @@ -209,7 +238,7 @@ impl Handler for AudioSynthHandler { let result = self.create_audio(&request.speaker, &request.sentence, request.use_words, request.use_phonemes, - request.volume); + request.volume, request.speed); let mime_type = "audio/wav".parse::().unwrap(); @@ -233,11 +262,14 @@ impl AudioSynthHandler { // TODO: Return errors. /// Create audio from the sentence. fn create_audio(&self, speaker: &str, sentence: &str, use_words: bool, - use_phonemes: bool, volume: Option) -> Vec { + use_phonemes: bool, volume: Option, + speed: Option) -> Vec { match self.synthesizer.read() { Err(_) => Vec::new(), // TODO Actual error. Ok(synth) => { - match synth.generate(sentence, speaker, use_words, use_phonemes, volume) { + let generated = synth.generate(sentence, speaker, use_words, + use_phonemes, volume, speed); + match generated { Err(e) => { println!("Error synthesizing: {:?}", e); Vec::new() // TODO FIXME diff --git a/src/main.rs b/src/main.rs index e14c1ec52b..3b74d19f87 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ pub mod arpabet; pub mod audiobank; pub mod config; pub mod dictionary; +pub mod effects; pub mod error; pub mod handlers; pub mod logger; diff --git a/src/synthesizer.rs b/src/synthesizer.rs index 9a4d9970f7..f8715222cb 100644 --- a/src/synthesizer.rs +++ b/src/synthesizer.rs @@ -2,10 +2,11 @@ use arpabet::ArpabetDictionary; use audiobank::Audiobank; +use effects::speed::change_speed; +use effects::volume::change_volume; use error::SynthError; use hound::WavSpec; use hound::WavWriter; -use std::i16; use std::io::BufWriter; use std::io::Cursor; use words::split_sentence; @@ -42,7 +43,8 @@ impl Synthesizer { */ pub fn generate(&self, sentence: &str, speaker: &str, use_words: bool, use_phonemes: bool, - volume: Option) -> Result { + volume: Option, speed: Option) + -> Result { let mut words = split_sentence(sentence); @@ -79,18 +81,19 @@ impl Synthesizer { }); } - // Adjust the volume of the waveform. - // FIXME: Super inefficient. - let mut pcm_data : Vec = Vec::with_capacity(concatenated_waveform.len()); - for x in &concatenated_waveform { - let data = raise_volume(*x, volume); - pcm_data.push(data); + // FIXME: Super inefficient pieces. + if speed.is_some() { + concatenated_waveform = change_speed(concatenated_waveform, speed.unwrap()); + } + + if volume.is_some() { + concatenated_waveform = change_volume(concatenated_waveform, volume.unwrap()); } // TODO: Cache waveform headers. let spec = try!(self.audiobank.get_misc_spec("pause")); - Ok(self.write_buffer(&spec, pcm_data)) + Ok(self.write_buffer(&spec, concatenated_waveform)) } /// Concatenate a word to the waveform we're building. Returns @@ -202,25 +205,3 @@ impl Synthesizer { } } -/// Raise the volume of a sample by changing its amplitude. -fn raise_volume(data: i16, volume: Option) -> i16 { - // TODO: Cleanup, make more efficient. - match volume { - None => data, - Some(vol) => { - let f : f32 = data as f32 * vol; - let g = f as i32; - - let h : i16 = if g > i16::MAX as i32 { - i16::MAX - } else if g < i16::MIN as i32 { - i16::MIN - } else { - g as i16 - }; - - h - }, - } -} - diff --git a/src/web/js/events.js b/src/web/js/events.js index 3463681196..fab57fe6f5 100644 --- a/src/web/js/events.js +++ b/src/web/js/events.js @@ -39,16 +39,18 @@ sentence = $('#text').val(), speaker = $('select').val(), volume = $('input#vol').val(), + speed = $('input#speed').val(), query = encodeURIComponent(sentence), url = '/speak?v=' + speaker + '&s=' + query + '&vol=' + volume + + '&spd=' + speed + '&up=' + $('#use_phonemes').prop('checked') + '&uw=' + $('#use_words').prop('checked'); ev.preventDefault(); - Url.setState(speaker, sentence, volume); + Url.setState(speaker, sentence, volume, speed); Sound.play(url); return false; diff --git a/src/web/js/initialize.js b/src/web/js/initialize.js index b4e1a0540c..90ada043d7 100644 --- a/src/web/js/initialize.js +++ b/src/web/js/initialize.js @@ -16,6 +16,7 @@ var sentence = Url.getSentence(), speaker = Url.getSpeaker() || Library.getDefaultSpeaker(), volume = Url.getVolume() || Volume.DEFAULT, + speed = Url.getSpeed() || 1.0, urlPreviousState = false; if (speaker) { @@ -28,6 +29,7 @@ } $('input#vol').val(volume); + $('input#speed').val(speed); $('#use_phonemes').prop('use_phonemes', Url.getUsePhonemes()); $('#use_words').prop('use_words', Url.getUseWords()); diff --git a/src/web/js/url.js b/src/web/js/url.js index 1312213c7d..4796bc4ab0 100644 --- a/src/web/js/url.js +++ b/src/web/js/url.js @@ -13,6 +13,9 @@ /** JSON key for the `volume` param. */ _VOLUME_KEY: 'vol', + /** JSON key for the `speed` param. */ + _SPEED_KEY: 'spd', + /** JSON key for the `use_phonemes` param. */ _USE_PHONEMES_KEY: 'up', @@ -51,6 +54,16 @@ } }, + /** Get the speed from the `window.location`. */ + getSpeed: function() { + var state = this.parseState(window.location); + if (!state || !(this._SPEED_KEY in state)) { + return null; + } else { + return state[this._SPEED_KEY]; + } + }, + /** Get the "use phonemes" from the `window.location`. */ getUsePhonemes: function() { var state = this.parseState(window.location); @@ -73,14 +86,14 @@ // TODO: Independent functions to set both speaker and sentence separately. /** Set the `window.history` speaker, sentence, phoneme use, and word use. */ - setState: function(speaker, rawSentence, volume, usePhonemes, useWords) { - var urlHash = this.fromParams(speaker, rawSentence, volume, usePhonemes, useWords); + setState: function(speaker, rawSentence, volume, speed, usePhonemes, useWords) { + var urlHash = this.fromParams(speaker, rawSentence, volume, speed, usePhonemes, useWords); console.log('set state, urlhash = ', urlHash); window.history.replaceState(null, null, urlHash); }, /** Encode a speaker and sentence in a URL hash. */ - fromParams: function(speaker, rawSentence, volume, usePhonemes, useWords) { + fromParams: function(speaker, rawSentence, volume, speed, usePhonemes, useWords) { var cleanedSentence = SentenceHelper.cleanSentence(rawSentence), cleanedSpeaker = speaker, // TODO: Check against speakers. cleanedVolume = volume, // TODO: Filter invalid values. @@ -91,6 +104,7 @@ state[this._SPEAKER_KEY] = cleanedSpeaker; state[this._SENTENCE_KEY] = cleanedSentence; state[this._VOLUME_KEY] = cleanedVolume; + state[this._SPEED_KEY] = speed; // TODO: Filter invalid values. state[this._USE_PHONEMES_KEY] = !!usePhonemes; state[this._USE_WORDS_KEY] = !!useWords; diff --git a/web/index.html b/web/index.html index 08f28e2244..164398b04d 100644 --- a/web/index.html +++ b/web/index.html @@ -22,6 +22,7 @@ +
@@ -30,6 +31,11 @@ checked="checked">
+
+ + +
+