Scid  4.7.0
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros
sound.tcl
Go to the documentation of this file.
1 ### sound.tcl
2 ### Functions for playing sound files to announce moves.
3 ### Part of Scid. Copyright (C) Shane Hudson 2004.
4 ### Copyright (C) 2013 Fulvio Benini
5 ###
6 ### Uses the free Tcl/Tk sound package "Snack", which comes with
7 ### most Tcl distributions. See http://www.speech.kth.se/snack/
8 
9 ### when an other application uses the audio device, no sound can be played. Forces a reset of pending sounds after 5 seconds
10 ### which limits the maximum length of a playable sound
11 
12 namespace eval ::utils::sound {}
13 
14 set ::utils::sound::pipe ""
15 set ::utils::sound::hasSound 0
16 set ::utils::sound::isPlayingSound 0
17 set ::utils::sound::soundQueue {}
18 set ::utils::sound::soundFiles [list \
19  King Queen Rook Bishop Knight CastleQ CastleK Back Mate Promote Check \
20  a b c d e f g h x 1 2 3 4 5 6 7 8 move alert]
21 
22 # soundMap
23 #
24 # Maps characters in a move to sounds.
25 # Before this map is used, "O-O-O" is converted to "q" and "O-O" to "k"
26 # Also note that "U" (undo) is used for taking back a move.
27 #
28 array set ::utils::sound::soundMap {
29  K King Q Queen R Rook B Bishop N Knight k CastleK q CastleQ
30  x x U Back # Mate = Promote + Check alert alert
31  a a b b c c d d e e f f g g h h
32  1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8
33 }
34 
35 
36 # ::utils::sound::Setup
37 #
38 # Called once at startup to load the Snack package and set up sounds.
39 #
40 proc ::utils::sound::Setup {} {
41  variable hasSound
42  variable soundFiles
43  variable soundFolder
44 
45  set hasSound 1
46  if {[catch {package require snack 2.0}]} {
47  if {$::windowsOS} {
48  catch {
49  set ::utils::sound::pipe [open "| scidsnd.exe" "r+"]
50  fconfigure $::utils::sound::pipe -blocking 0 -buffering line
51  fileevent $::utils::sound::pipe readable {
52  gets $::utils::sound::pipe
53  ::utils::sound::SoundFinished
54  }
55  }
56  }
57  if { $::utils::sound::pipe == "" } { set hasSound 0}
58  } else {
59  # Set up sounds. Each sound will be empty until a WAV file for it is found.
60  foreach soundFile $soundFiles {
61  ::snack::sound sound_$soundFile
62  }
64  }
65 }
66 
67 
68 # ::utils::sound::ReadFolder
69 #
70 # Reads sound files from the specified directory.
71 # Returns the number of Scid sound files found in that directory.
72 #
73 proc ::utils::sound::ReadFolder {{newFolder ""}} {
74  variable soundFiles
75  variable soundFolder
76 
77  if {$newFolder != ""} { set soundFolder ""}
78 
79  set count 0
80  foreach soundFile $soundFiles {
81  set f [file join $soundFolder $soundFile.wav]
82  if {[file readable $f]} {
83  if { $::utils::sound::pipe == "" } {
84  sound_$soundFile configure -file $f
85  }
86  incr count
87  }
88  }
89  return $count
90 }
91 
92 
93 
94 proc ::utils::sound::AnnounceMove {move} {
95  variable hasSound
96  variable soundMap
97 
98  if {! $hasSound} { return}
99 
100  if {[string range $move 0 4] == "O-O-O"} { set move q}
101  if {[string range $move 0 2] == "O-O"} { set move k}
102  set move [::untrans $move]
103  set parts [split $move ""]
104  set soundList {}
105  foreach part $parts {
106  if {[info exists soundMap($part)]} {
107  lappend soundList sound_$soundMap($part)
108  }
109  }
110  if {[llength $soundList] > 0} {
112  foreach s $soundList {
113  PlaySound $s
114  }
115  }
116 }
117 
118 
119 proc ::utils::sound::AnnounceNewMove {move} {
120  if {$::utils::sound::announceNew} { AnnounceMove $move}
121 }
122 
123 
124 proc ::utils::sound::AnnounceForward {move} {
125  if {$::utils::sound::announceForward} { AnnounceMove $move}
126 }
127 
128 
129 proc ::utils::sound::AnnounceBack {} {
130  if {$::utils::sound::announceBack} { AnnounceMove U}
131 }
132 
133 
134 proc ::utils::sound::SoundFinished {} {
135  after cancel ::utils::sound::CancelSounds
136  set ::utils::sound::isPlayingSound 0
138 }
139 
140 
141 proc ::utils::sound::CancelSounds {} {
142  if {! $::utils::sound::hasSound} { return}
143 
144  if { $::utils::sound::pipe != "" } {
145  puts $::utils::sound::pipe "stop"
146  } else {
147  snack::audio stop
148  }
149  set ::utils::sound::soundQueue {}
150  set ::utils::sound::isPlayingSound 0
151 }
152 
153 ################################################################################
154 #
155 ################################################################################
156 proc ::utils::sound::PlaySound {sound} {
157  if {! $::utils::sound::hasSound} { return}
158  lappend ::utils::sound::soundQueue $sound
159  after idle ::utils::sound::CheckSoundQueue
160 }
161 
162 # ::utils::sound::CheckSoundQueue
163 #
164 # Starts playing the next available sound, if there is one waiting
165 # and no sound is currently playing. Called whenever a sound is
166 # added to the queue or a sound has finished playing.
167 #
168 proc ::utils::sound::CheckSoundQueue {} {
169  variable soundQueue
170  variable isPlayingSound
171  if {$isPlayingSound} { return}
172  if {[llength $soundQueue] == 0} { return}
173 
174  set next [lindex $soundQueue 0]
175  set soundQueue [lrange $soundQueue 1 end]
176  set isPlayingSound 1
177  if { $::utils::sound::pipe != "" } {
178  set next [string range $next 6 end]
179  set f [file join $::utils::sound::soundFolder $next.wav]
180  puts $::utils::sound::pipe "[file nativename $f]"
181  } else {
182  catch { $next play -blocking 0 -command ::utils::sound::SoundFinished}
183  after 5000 ::utils::sound::CancelSounds
184  }
185 }
186 
187 
188 # ::utils::sound::OptionsDialog
189 #
190 # Dialog window for configuring move sounds.
191 #
192 # TODO: language translations for this dialog.
193 #
194 proc ::utils::sound::OptionsDialog {} {
195  set w .soundOptions
196 
197  foreach v {soundFolder announceNew announceForward announceBack} {
198  set ::utils::sound::${v}_temp [set ::utils::sound::$v]
199  }
200 
201  toplevel $w -background [ttk::style lookup . -background]
202  wm title $w "Scid: Sound Options"
203  # wm transient $w .
204 
205 
206  ttk::label $w.status -text ""
207  if {! $::utils::sound::hasSound} {
208  $w.status configure -text "Scid could not find the Snack audio package at startup; Sound is disabled."
209  pack $w.status -side bottom
210  }
211  pack [ttk::frame $w.b] -side bottom -fill x
212  pack [ttk::frame $w.f] -side top -fill x -padx 5 -pady 5 -ipadx 4 -ipady 4
213 
214  set f $w.f
215  set r 0
216 
217  ttk::label $f.ftitle -text $::tr(SoundsFolder) -font font_Bold
218  grid $f.ftitle -row $r -column 0 -columnspan 3 -pady 4
219  incr r
220 
221  ttk::entry $f.folderEntry -width 40 -textvariable ::utils::sound::soundFolder_temp
222  grid $f.folderEntry -row $r -column 0 -columnspan 2 -sticky we
223  ttk::button $f.folderBrowse -text " $::tr(Browse)... " \
224  -command ::utils::sound::OptionsDialogChooseFolder
225  grid $f.folderBrowse -row $r -column 2
226  incr r
227 
228  ttk::label $f.folderHelp -text $::tr(SoundsFolderHelp)
229  grid $f.folderHelp -row $r -column 0 -columnspan 3
230  incr r
231 
232  grid [ttk::frame $f.gap$r -height 5] -row $r -column -0; incr r
233 
234  ttk::label $f.title -text $::tr(SoundsAnnounceOptions) -font font_Bold
235  grid $f.title -row $r -column 0 -columnspan 3 -pady 4
236  incr r
237 
238  ttk::checkbutton $f.announceNew -text $::tr(SoundsAnnounceNew) \
239  -variable ::utils::sound::announceNew_temp
240  grid $f.announceNew -row $r -column 0 -columnspan 2 -sticky w
241  incr r
242 
243  grid [ttk::frame $f.gap$r -height 5] -row $r -column -0; incr r
244 
245  ttk::checkbutton $f.announceForward -text $::tr(SoundsAnnounceForward) \
246  -variable ::utils::sound::announceForward_temp
247  grid $f.announceForward -row $r -column 0 -columnspan 2 -sticky w
248  incr r
249 
250  grid [ttk::frame $f.gap$r -height 5] -row $r -column -0; incr r
251 
252  ttk::checkbutton $f.announceBack -text $::tr(SoundsAnnounceBack) \
253  -variable ::utils::sound::announceBack_temp
254  grid $f.announceBack -row $r -column 0 -columnspan 2 -sticky w
256  dialogbutton $w.b.ok -text OK -command ::utils::sound::OptionsDialogOK
257  dialogbutton $w.b.cancel -text $::tr(Cancel) -command [list destroy $w]
258  packbuttons right $w.b.cancel $w.b.ok
259  bind $w <Return> [list $w.b.ok invoke]
260  bind $w <Escape> [list $w.b.cancel invoke]
262  wm resizable $w 0 0
263  raiseWin $w
264  grab $w
265  focus $w.f.folderEntry
266 }
267 
268 proc ::utils::sound::OptionsDialogChooseFolder {} {
269  set newFolder [tk_chooseDirectory \
270  -initialdir $::utils::sound::soundFolder_temp \
271  -parent .soundOptions \
272  -title "Scid: $::tr(SoundsFolder)"]
273  if {$newFolder != ""} {
274  set ::utils::sound::soundFolder_temp [file nativename $newFolder]
275  }
276 }
277 
278 proc ::utils::sound::OptionsDialogOK {} {
279  variable soundFolder
280 
281  # Destroy the Sounds options dialog
282  set w .soundOptions
283  catch {grab release $w}
284  destroy $w
285 
286  set isNewSoundFolder 0
287  if {$soundFolder != $::utils::sound::soundFolder_temp} {
288  set isNewSoundFolder 1
289  }
290 
291  # Update the user-settable sound variables:
292  foreach v {soundFolder announceNew announceForward announceBack} {
293  set ::utils::sound::$v [set ::utils::sound::${v}_temp]
294  }
295 
296  # If the user selected a different folder to look in, read it
297  # and tell the user how many sound files were found there.
298 
299  if {$isNewSoundFolder && $soundFolder != ""} {
300  set numSoundFiles [::utils::sound::ReadFolder]
301  tk_messageBox -title "Scid: Sound Files" -type ok -icon info \
302  -message "Found $numSoundFiles of [llength $::utils::sound::soundFiles] sound files in $::utils::sound::soundFolder"
303  }
304 }
305 
306 # Read the sound files at startup: