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.
This commit is contained in:
Brandon Thomas committed 2016-09-01 02:04:41 -04:00
1 parent 740e3c7eec
commit 489aff2540
10 files changed
+144 -38

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
// Copyright (c) 2016 Brandon Thomas <bt@brand.io, echelon@gmail.com>
pub mod speed;
pub mod volume;
+28
View File
@@ -0,0 +1,28 @@
// Copyright (c) 2016 Brandon Thomas <bt@brand.io, echelon@gmail.com>
/**
* 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<i16>, speed: f32) -> Vec<i16> {
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<i16> = 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
}
+35
View File
@@ -0,0 +1,35 @@
// Copyright (c) 2016 Brandon Thomas <bt@brand.io, echelon@gmail.com>
use std::i16;
/**
* Change the volume of a waveform.
* TODO: Doc.
*/
pub fn change_volume(waveform: Vec<i16>, volume: f32) -> Vec<i16> {
let mut transformed : Vec<i16> = 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
}
+35 -3
View File
@@ -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<f32>,
/** An optional speed multiplier. */
pub speed: Option<f32>,
/** Whether to use phonemes. */
pub use_phonemes: bool,
@@ -122,6 +127,28 @@ impl SpeakRequest {
},
};
let speed : Option<f32> = match map.get(SPEED_PARAM) {
None => { None },
Some(list) => {
match list.get(0) {
None => { None },
Some(s) => {
match s.trim().parse::<f32>() {
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::<Mime>().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<f32>) -> Vec<u8> {
use_phonemes: bool, volume: Option<f32>,
speed: Option<f32>) -> Vec<u8> {
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
+1
View File
@@ -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;
+12 -31
View File
@@ -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<f32>) -> Result<WavBytes, SynthError> {
volume: Option<f32>, speed: Option<f32>)
-> Result<WavBytes, SynthError> {
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<i16> = 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<f32>) -> 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
},
}
}
+3 -1
View File
@@ -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;
+2
View File
@@ -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());
+17 -3
View File
@@ -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;
+6
View File
@@ -22,6 +22,7 @@
<input id="vol" value="">
<button id="submit">Submit</button>
</div>
<div id="options">
<input type="checkbox" id="use_words" name="use_words"
checked="checked">
@@ -30,6 +31,11 @@
checked="checked">
<label for="use_phonemes">Phonemes</label>
</div>
<div id="more_options">
<input id="speed" value="">
<label for="speed">Speed</label>
</div>
</form>
</div>
<ul id="stats">