mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 00:47:53 +00:00
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:
1 parent
740e3c7eec
commit
489aff2540
10 files changed
+144
-38
No files matched your search
@@ -0,0 +1,5 @@
|
||||
// Copyright (c) 2016 Brandon Thomas <bt@brand.io, echelon@gmail.com>
|
||||
|
||||
pub mod speed;
|
||||
pub mod volume;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user