1   package org.daisy.pipeline.tts;
2   
3   import java.util.Arrays;
4   import java.util.Collection;
5   import java.util.Collections;
6   import java.util.HashMap;
7   import java.util.List;
8   import java.util.Locale;
9   import java.util.Map;
10  import java.util.Optional;
11  
12  import com.google.common.collect.Collections2;
13  
14  import org.daisy.pipeline.tts.VoiceInfo.Gender;
15  import org.daisy.pipeline.tts.VoiceInfo.LanguageRange;
16  
17  /**
18   * Voice instances are expected to be created only by TTSEngine/TTSService implementations
19   */
20  public class Voice {
21  
22  	// the upper-case versions need to be kept because some TTS Processors like SAPI
23  	// are case-sensitive. Lower-case versions are only used for comparison.
24  	private final String engine;
25  	private final String name;
26  	private final MarkSupport markSupport;
27  	private final Collection<LanguageRange> locale;
28  	private final Optional<Gender> gender;
29  	private final String id;
30  
31  	public enum MarkSupport {
32  		DEFAULT,
33  		MARK_SUPPORTED,
34  		MARK_NOT_SUPPORTED
35  	}
36  
37  	public Voice(String engine, String name) {
38  		this(engine, name, Collections.emptyList(), null, MarkSupport.DEFAULT);
39  	}
40  
41  	/**
42  	 * @throws IllegalArgumentException if language or gender can not be parsed.
43  	 */
44  	public Voice(String engine, String name, Locale locale, Gender gender) throws IllegalArgumentException {
45  		this(engine,
46  		     name,
47  		     locale != null ? new LanguageRange(locale) : null,
48  		     gender);
49  	}
50  
51  	public Voice(String engine, String name, LanguageRange locale, Gender gender) throws IllegalArgumentException {
52  		this(engine,
53  		     name,
54  		     Optional.ofNullable(locale).map(x -> listOf(x)).orElseGet(Collections::emptyList),
55  		     gender,
56  		     MarkSupport.DEFAULT);
57  	}
58  
59  	public Voice(String engine, String name, Collection<Locale> locale, Gender gender) throws IllegalArgumentException {
60  		this(engine, name, Collections2.transform(locale, LanguageRange::new), gender, MarkSupport.DEFAULT);
61  	}
62  
63  	public Voice(String engine, String name, Collection<LanguageRange> locale, Gender gender, MarkSupport markSupport)
64  			throws IllegalArgumentException {
65  		if (engine == null || engine.isEmpty() || name == null || name.isEmpty())
66  			throw new IllegalArgumentException();
67  		// we keep the strings in their full case form because some engines might be case sensitive
68  		this.engine = engine;
69  		this.name = name;
70  		this.locale = Collections.unmodifiableCollection(locale);
71  		this.gender = Optional.ofNullable(gender);
72  		this.markSupport = markSupport;
73  		synchronized (ids) {
74  			if (ids.containsKey(this))
75  				id = ids.get(this);
76  			else {
77  				Integer i = voiceCounter.get(engine.toLowerCase());
78  				if (i == null)
79  					i = 1;
80  				else
81  					i++;
82  				id = engine.toLowerCase() + "-" + i;
83  				ids.put(this, id);
84  				voiceCounter.put(engine.toLowerCase(), i);
85  			}
86  		}
87  	}
88  
89  	private final static Map<Voice,String> ids = new HashMap<>();
90  	private final static Map<String,Integer> voiceCounter = new HashMap<>();
91  
92  	// see List.of() method in Java 9
93  	private static <E> List<E> listOf(E element) {
94  		if (element == null)
95  			throw new NullPointerException();
96  		return Collections.unmodifiableList(Arrays.asList(element));
97  	}
98  
99  	public String getName() {
100 		return name;
101 	}
102 
103 	public String getEngine() {
104 		return engine;
105 	}
106 
107 	public MarkSupport getMarkSupport() {
108 		return markSupport;
109 	}
110 
111 	/**
112 	 * The language range(s) of this voice, or empty if it is not known.
113 	 */
114 	public Collection<LanguageRange> getLocale() {
115 		return locale;
116 	}
117 
118 	/**
119 	 * The gender of this voice, or absent if it is not known.
120 	 */
121 	public Optional<Gender> getGender() {
122 		return gender;
123 	}
124 
125 	/**
126 	 * A unique ID.
127 	 *
128 	 * This method is consistent with {@link #equals}: it returns the same ID for equal voices,
129 	 * and returns distinct IDs for distinct voices.
130 	 */
131 	public String getID() {
132 		return id;
133 	}
134 
135 	public int hashCode() {
136 		return engine.toLowerCase().hashCode() ^ name.toLowerCase().hashCode();
137 	}
138 
139 	public boolean equals(Object other) {
140 		if (other == null)
141 			return false;
142 		Voice v2 = (Voice)other;
143 		return engine.equalsIgnoreCase(v2.engine) && name.equalsIgnoreCase(v2.name);
144 	}
145 
146 	public String toString() {
147 		StringBuilder s = new StringBuilder();
148 		s.append("{");
149 		s.append("engine: ").append(engine);
150 		s.append(", ").append("name: ").append(name);
151 		if (!locale.isEmpty()) {
152 			s.append(", ").append("locale: ");
153 			if (locale.size() > 1) s.append("\"");
154 			s.append(LanguageRange.toString(locale));
155 			if (locale.size() > 1) s.append("\"");
156 		}
157 		if (gender.isPresent()) {
158 			s.append(", ").append("gender:").append(gender.get());
159 		}
160 		s.append("}");
161 		return s.toString();
162 	}
163 }