```
Usage: java org.openhab.voice.voicerss.tool.CreateTTSCache <args>
-Arguments: --api-key <key> <cache-dir> <locale> { <text> | @inputfile }
+Arguments: --api-key <key> <cache-dir> <locale> <voice> { <text> | @inputfile } [ <codec> <format> ]
key the VoiceRSS API Key, e.g. "123456789"
cache-dir is directory where the files will be stored, e.g. "voicerss-cache"
locale the language locale, has to be valid, e.g. "en-us", "de-de"
+ voice the voice, "default" for the default voice
text the text to create audio file for, e.g. "Hello World"
inputfile a name of a file, where all lines will be translatet to text, e.g. "@message.txt"
+ codec the audio codec, "MP3", "WAV", "OGG" or "AAC", "MP3" by default
+ format the audio format, "44khz_16bit_mono" by default
-Sample: java org.openhab.voice.voicerss.tool.CreateTTSCache --api-key 1234567890 cache en-US @messages.txt
+Sample: java org.openhab.voice.voicerss.tool.CreateTTSCache --api-key 1234567890 cache en-US default @messages.txt
```
-
-## Open Issues
-
-* do not log API-Key in plain text
+You will need to specify the classpath for your addon (jar) in the command line (java -cp <path> ...).
import java.io.File;
+import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.audio.AudioException;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioStream;
*
* @author Jochen Hiller - Initial contribution and API
*/
+@NonNullByDefault
class VoiceRSSAudioStream extends FileAudioStream {
public VoiceRSSAudioStream(File audioFile, AudioFormat format) throws AudioException {
* @return The audio formats of this instance
*/
private Set<AudioFormat> initAudioFormats() {
- return voiceRssImpl.getAvailableAudioFormats();
+ Set<AudioFormat> audioFormats = new HashSet<>();
+ for (String codec : voiceRssImpl.getAvailableAudioCodecs()) {
+ switch (codec) {
+ case "MP3":
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_MP3, null, 16, 64000,
+ 44_100L));
+ break;
+ case "OGG":
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_OGG, AudioFormat.CODEC_VORBIS, null, 16,
+ null, 44_100L));
+ break;
+ case "AAC":
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_AAC, null, 16, null,
+ 44_100L));
+ break;
+ case "WAV":
+ // Consider only mono formats
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false,
+ 8, 64_000, 8_000L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false,
+ 16, 128_000, 8_000L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false,
+ 8, 88_200, 11_025L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false,
+ 16, 176_400, 11_025L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false,
+ 8, 96_000, 12_000L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false,
+ 16, 192_000, 12_000L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false,
+ 8, 128_000, 16_000L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false,
+ 16, 256_000, 16_000L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false,
+ 8, 176_400, 22_050L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false,
+ 16, 352_800, 22_050L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false,
+ 8, 192_000, 24_000L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false,
+ 16, 384_000, 24_000L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false,
+ 8, 256_000, 32_000L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false,
+ 16, 512_000, 32_000L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false,
+ 8, 352_800, 44_100L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false,
+ 16, 705_600, 44_100L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false,
+ 8, 384_000, 48_000L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false,
+ 16, 768_000, 48_000L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ALAW, null, 8,
+ 64_000, 8_000L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ALAW, null, 8,
+ 88_200, 11_025L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ALAW, null, 8,
+ 176_400, 22_050L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ALAW, null, 8,
+ 352_800, 44_100L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ULAW, null, 8,
+ 64_000, 8_000L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ULAW, null, 8,
+ 88_200, 11_025L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ULAW, null, 8,
+ 176_400, 22_050L));
+ audioFormats.add(new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ULAW, null, 8,
+ 352_800, 44_100L));
+ break;
+ default:
+ logger.debug("Audio codec {} not yet supported", codec);
+ break;
+ }
+ }
+ return audioFormats;
}
/**
}
private CachedVoiceRSSCloudImpl initVoiceImplementation() throws IllegalStateException {
- return new CachedVoiceRSSCloudImpl(getCacheFolderName());
+ return new CachedVoiceRSSCloudImpl(getCacheFolderName(), true);
}
private String getCacheFolderName() {
import java.util.Locale;
+import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.voice.Voice;
import org.openhab.voice.voicerss.internal.cloudapi.VoiceRSSCloudImpl;
*
* @author Jochen Hiller - Initial contribution and API
*/
+@NonNullByDefault
public class VoiceRSSVoice implements Voice {
/**
import java.security.NoSuchAlgorithmException;
import java.util.Objects;
-import org.slf4j.Logger;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.LoggerFactory;
/**
*
* @author Jochen Hiller - Initial contribution
*/
+@NonNullByDefault
public class CachedVoiceRSSCloudImpl extends VoiceRSSCloudImpl {
/**
*/
private static final int READ_BUFFER_SIZE = 4096;
- private final Logger logger = LoggerFactory.getLogger(CachedVoiceRSSCloudImpl.class);
-
private final File cacheFolder;
- public CachedVoiceRSSCloudImpl(String cacheFolderName) throws IllegalStateException {
- if (cacheFolderName == null) {
+ public CachedVoiceRSSCloudImpl(String cacheFolderName, boolean logging) throws IllegalStateException {
+ super(logging);
+ if (cacheFolderName.isBlank()) {
throw new IllegalStateException("Folder for cache must be defined");
}
// Lazy create the cache folder
*
* Sample: "en-US_00a2653ac5f77063bc4ea2fee87318d3"
*/
- private String getUniqueFilenameForText(String text, String locale, String voice, String format) {
+ private @Nullable String getUniqueFilenameForText(String text, String locale, String voice, String format) {
try {
byte[] bytesOfMessage = text.getBytes(StandardCharsets.UTF_8);
MessageDigest md = MessageDigest.getInstance("MD5");
return filename;
} catch (NoSuchAlgorithmException ex) {
// should not happen
- logger.error("Could not create MD5 hash for '{}'", text, ex);
+ if (logging) {
+ LoggerFactory.getLogger(CachedVoiceRSSCloudImpl.class).error("Could not create MD5 hash for '{}'", text,
+ ex);
+ }
return null;
}
}
import java.util.Locale;
import java.util.Set;
-import org.openhab.core.audio.AudioFormat;
+import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Interface which represents the functionality needed from the VoiceRSS TTS
*
* @author Jochen Hiller - Initial contribution
*/
+@NonNullByDefault
public interface VoiceRSSCloudAPI {
/**
Set<Locale> getAvailableLocales();
/**
- * Get all supported audio formats by the TTS service. This includes MP3,
- * WAV and more audio formats as used in APIs. About supported audio
- * formats, see {@link AudioFormat}
+ * Get all supported audio codecs by the TTS service. This includes MP3,
+ * WAV and more audio formats as used in APIs.
*
- * @return A set of all audio formats supported
+ * @return A set of all audio codecs supported
*/
- Set<AudioFormat> getAvailableAudioFormats();
+ Set<String> getAvailableAudioCodecs();
/**
* Get all supported voices.
import java.util.Map.Entry;
import java.util.Set;
-import org.openhab.core.audio.AudioFormat;
+import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
* @author Laurent Garnier - add support for OGG and AAC audio formats
* @author Andreas Brenk - add support for WAV audio format
*/
+@NonNullByDefault
public class VoiceRSSCloudImpl implements VoiceRSSCloudAPI {
public static final String DEFAULT_VOICE = "default";
public static final String API_URL = "https://api.voicerss.org/?key=%s&hl=%s&c=%s&f=%s&src=%s";
public static final String API_URL_WITH_VOICE = API_URL + "&v=%s";
- private static final Set<AudioFormat> SUPPORTED_AUDIO_FORMATS = Set.of(
- new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_MP3, null, 16, null, 44_100L),
- new AudioFormat(AudioFormat.CONTAINER_OGG, AudioFormat.CODEC_VORBIS, null, 16, null, 44_100L),
- new AudioFormat(AudioFormat.CONTAINER_NONE, AudioFormat.CODEC_AAC, null, 16, null, 44_100L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, null, 8, 64_000, 8_000L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, null, 16, 128_000, 8_000L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 8, 88_200, 11_025L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 176_400, 11_025L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 8, 96_000, 12_000L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 192_000, 12_000L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 8, 128_000, 16_000L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 256_000, 16_000L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 8, 176_400, 22_050L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 352_800, 22_050L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 8, 192_000, 24_000L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 384_000, 24_000L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 8, 256_000, 32_000L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 512_000, 32_000L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 8, 352_800, 44_100L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 705_600, 44_100L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 8, 384_000, 48_000L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 768_000, 48_000L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ALAW, null, 8, 64_000, 8_000L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ALAW, null, 8, 88_200, 11_025L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ALAW, null, 8, 176_400, 22_050L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ALAW, null, 8, 352_800, 44_100L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ULAW, null, 8, 64_000, 8_000L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ULAW, null, 8, 88_200, 11_025L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ULAW, null, 8, 176_400, 22_050L),
- new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_ULAW, null, 8, 352_800, 44_100L));
+ private static final Set<String> SUPPORTED_AUDIO_CODECS = Set.of("MP3", "OGG", "AAC", "WAV", "CAF");
private static final Set<Locale> SUPPORTED_LOCALES = new HashSet<>();
static {
SUPPORTED_VOICES.put("zh-tw", Set.of("Akemi", "Lin", "Lee"));
}
- private final Logger logger = LoggerFactory.getLogger(VoiceRSSCloudImpl.class);
+ protected boolean logging;
+
+ public VoiceRSSCloudImpl(boolean logging) {
+ this.logging = logging;
+ }
@Override
- public Set<AudioFormat> getAvailableAudioFormats() {
- return SUPPORTED_AUDIO_FORMATS;
+ public Set<String> getAvailableAudioCodecs() {
+ return SUPPORTED_AUDIO_CODECS;
}
@Override
public InputStream getTextToSpeech(String apiKey, String text, String locale, String voice, String audioCodec,
String audioFormat) throws IOException {
String url = createURL(apiKey, text, locale, voice, audioCodec, audioFormat);
- logger.debug("Call {}", url.replace(apiKey, "***"));
+ if (logging) {
+ LoggerFactory.getLogger(VoiceRSSCloudImpl.class).debug("Call {}", url.replace(apiKey, "***"));
+ }
URLConnection connection = new URL(url).openConnection();
// we will check return codes. The service will ALWAYS return a HTTP
// the error message in body
int status = ((HttpURLConnection) connection).getResponseCode();
if (HttpURLConnection.HTTP_OK != status) {
- logger.warn("Call {} returned HTTP {}", url.replace(apiKey, "***"), status);
+ if (logging) {
+ LoggerFactory.getLogger(VoiceRSSCloudImpl.class).warn("Call {} returned HTTP {}",
+ url.replace(apiKey, "***"), status);
+ }
throw new IOException("Could not read from service: HTTP code " + status);
}
- if (logger.isTraceEnabled()) {
- for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
- logger.trace("Response.header: {}={}", header.getKey(), header.getValue());
+ if (logging) {
+ Logger logger = LoggerFactory.getLogger(VoiceRSSCloudImpl.class);
+ if (logger.isTraceEnabled()) {
+ for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
+ logger.trace("Response.header: {}={}", header.getKey(), header.getValue());
+ }
}
}
String contentType = connection.getHeaderField("Content-Type");
try {
is.close();
} catch (IOException ex) {
- logger.debug("Failed to close inputstream", ex);
+ if (logging) {
+ LoggerFactory.getLogger(VoiceRSSCloudImpl.class).debug("Failed to close inputstream", ex);
+ }
}
throw new IOException(
"Could not read audio content, service returned an error: " + new String(bytes, "UTF-8"));
import java.io.FileReader;
import java.io.IOException;
+import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.voice.voicerss.internal.cloudapi.CachedVoiceRSSCloudImpl;
/**
*
* @author Jochen Hiller - Initial contribution
*/
+@NonNullByDefault
public class CreateTTSCache {
public static final int RC_OK = 0;
public static final int RC_USAGE = 1;
public static final int RC_INPUT_FILE_NOT_FOUND = 2;
public static final int RC_API_KEY_MISSING = 3;
+ public static final int RC_INVALID_CODEC = 4;
public static void main(String[] args) throws IOException {
CreateTTSCache tool = new CreateTTSCache();
}
public int doMain(String[] args) throws IOException {
- if ((args == null) || (args.length != 5)) {
+ if (args.length < 6) {
usage();
return RC_USAGE;
}
String cacheDir = args[2];
String locale = args[3];
String voice = args[4];
+ String codec = "MP3";
+ if (args.length >= 7) {
+ switch (args[6]) {
+ case "MP3":
+ case "WAV":
+ case "OGG":
+ case "AAC":
+ codec = args[6];
+ break;
+ default:
+ usage();
+ return RC_INVALID_CODEC;
+ }
+ }
+ String format = args.length >= 8 ? args[7] : "44khz_16bit_mono";
if (args[5].startsWith("@")) {
String inputFileName = args[5].substring(1);
File inputFile = new File(inputFileName);
System.err.println("File " + inputFileName + " not found");
return RC_INPUT_FILE_NOT_FOUND;
}
- generateCacheForFile(apiKey, cacheDir, locale, voice, inputFileName);
+ generateCacheForFile(apiKey, cacheDir, locale, voice, codec, format, inputFileName);
} else {
String text = args[5];
- generateCacheForMessage(apiKey, cacheDir, locale, voice, text);
+ generateCacheForMessage(apiKey, cacheDir, locale, voice, codec, format, text);
}
return RC_OK;
}
private void usage() {
System.out.println("Usage: java org.openhab.voice.voicerss.tool.CreateTTSCache <args>");
- System.out.println("Arguments: --api-key <key> <cache-dir> <locale> { <text> | @inputfile }");
+ System.out.println(
+ "Arguments: --api-key <key> <cache-dir> <locale> <voice> { <text> | @inputfile } [ <codec> <format> ]");
System.out.println(" key the VoiceRSS API Key, e.g. \"123456789\"");
System.out.println(" cache-dir is directory where the files will be stored, e.g. \"voicerss-cache\"");
System.out.println(" locale the language locale, has to be valid, e.g. \"en-us\", \"de-de\"");
System.out.println(" text the text to create audio file for, e.g. \"Hello World\"");
System.out.println(
" inputfile a name of a file, where all lines will be translatet to text, e.g. \"@message.txt\"");
+ System.out.println(" codec the audio codec, \"MP3\", \"WAV\", \"OGG\" or \"AAC\", \"MP3\" by default");
+ System.out.println(" format the audio format, \"44khz_16bit_mono\" by default");
System.out.println();
System.out.println(
- "Sample: java org.openhab.voice.voicerss.tool.CreateTTSCache --api-key 1234567890 cache en-US @messages.txt");
+ "Sample: java org.openhab.voice.voicerss.tool.CreateTTSCache --api-key 1234567890 cache en-US default @messages.txt");
System.out.println();
}
- private void generateCacheForFile(String apiKey, String cacheDir, String locale, String voice, String inputFileName)
- throws IOException {
+ private void generateCacheForFile(String apiKey, String cacheDir, String locale, String voice, String codec,
+ String format, String inputFileName) throws IOException {
File inputFile = new File(inputFileName);
try (BufferedReader br = new BufferedReader(new FileReader(inputFile))) {
String line;
while ((line = br.readLine()) != null) {
// process the line.
- generateCacheForMessage(apiKey, cacheDir, locale, voice, line);
+ generateCacheForMessage(apiKey, cacheDir, locale, voice, codec, format, line);
}
}
}
- private void generateCacheForMessage(String apiKey, String cacheDir, String locale, String voice, String msg)
- throws IOException {
- if (msg == null) {
- System.err.println("Ignore msg=null");
- return;
- }
+ private void generateCacheForMessage(String apiKey, String cacheDir, String locale, String voice, String codec,
+ String format, String msg) throws IOException {
String trimmedMsg = msg.trim();
if (trimmedMsg.length() == 0) {
System.err.println("Ignore msg=''");
return;
}
- CachedVoiceRSSCloudImpl impl = new CachedVoiceRSSCloudImpl(cacheDir);
- File cachedFile = impl.getTextToSpeechAsFile(apiKey, trimmedMsg, locale, voice, "MP3", null);
- System.out.println(
- "Created cached audio for locale='" + locale + "', msg='" + trimmedMsg + "' to file=" + cachedFile);
+ try {
+ CachedVoiceRSSCloudImpl impl = new CachedVoiceRSSCloudImpl(cacheDir, false);
+ File cachedFile = impl.getTextToSpeechAsFile(apiKey, trimmedMsg, locale, voice, codec, format);
+ System.out.println("Created cached audio for locale='" + locale + "', voice='" + voice + "', msg='"
+ + trimmedMsg + "' to file=" + cachedFile);
+ } catch (IllegalStateException | IOException ex) {
+ System.err.println("Failed to create cached audio for locale='" + locale + "', voice='" + voice + "',msg='"
+ + trimmedMsg + "'");
+ }
}
}