Scid  4.6.5
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
202  wm title $w "Scid: Sound Options"
203  # wm transient $w .
204 
205 
206  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 [frame $w.b] -side bottom -fill x -pady 2
212  pack [frame $w.f -relief groove -borderwidth 2] \
213  -side top -fill x -padx 5 -pady 5 -ipadx 4 -ipady 4
214 
215  set f $w.f
216  set r 0
217 
218  label $f.ftitle -text $::tr(SoundsFolder) -font font_Bold
219  grid $f.ftitle -row $r -column 0 -columnspan 3 -pady 4
220  incr r
221 
222  entry $f.folderEntry -width 40 -textvariable ::utils::sound::soundFolder_temp
223  grid $f.folderEntry -row $r -column 0 -columnspan 2 -sticky we
224  button $f.folderBrowse -text " $::tr(Browse)... " \
225  -command ::utils::sound::OptionsDialogChooseFolder
226  grid $f.folderBrowse -row $r -column 2
227  incr r
228 
229  label $f.folderHelp -text $::tr(SoundsFolderHelp)
230  grid $f.folderHelp -row $r -column 0 -columnspan 3
231  incr r
232 
233  grid [frame $f.gap$r -height 5] -row $r -column -0; incr r
234 
235  label $f.title -text $::tr(SoundsAnnounceOptions) -font font_Bold
236  grid $f.title -row $r -column 0 -columnspan 3 -pady 4
237  incr r
238 
239  checkbutton $f.announceNew -text $::tr(SoundsAnnounceNew) \
240  -variable ::utils::sound::announceNew_temp
241  grid $f.announceNew -row $r -column 0 -columnspan 2 -sticky w
242  incr r
243 
244  grid [frame $f.gap$r -height 5] -row $r -column -0; incr r
245 
246  checkbutton $f.announceForward -text $::tr(SoundsAnnounceForward) \
247  -variable ::utils::sound::announceForward_temp
248  grid $f.announceForward -row $r -column 0 -columnspan 2 -sticky w
249  incr r
250 
251  grid [frame $f.gap$r -height 5] -row $r -column -0; incr r
252 
253  checkbutton $f.announceBack -text $::tr(SoundsAnnounceBack) \
254  -variable ::utils::sound::announceBack_temp
255  grid $f.announceBack -row $r -column 0 -columnspan 2 -sticky w
256  incr r
257 
258  dialogbutton $w.b.ok -text OK -command ::utils::sound::OptionsDialogOK
259  dialogbutton $w.b.cancel -text $::tr(Cancel) -command [list destroy $w]
260  packbuttons right $w.b.cancel $w.b.ok
261  bind $w <Return> [list $w.b.ok invoke]
262  bind $w <Escape> [list $w.b.cancel invoke]
264  wm resizable $w 0 0
265  raiseWin $w
266  grab $w
267  focus $w.f.folderEntry
268 }
269 
270 proc ::utils::sound::OptionsDialogChooseFolder {} {
271  set newFolder [tk_chooseDirectory \
272  -initialdir $::utils::sound::soundFolder_temp \
273  -parent .soundOptions \
274  -title "Scid: $::tr(SoundsFolder)"]
275  if {$newFolder != ""} {
276  set ::utils::sound::soundFolder_temp [file nativename $newFolder]
277  }
278 }
279 
280 proc ::utils::sound::OptionsDialogOK {} {
281  variable soundFolder
282 
283  # Destroy the Sounds options dialog
284  set w .soundOptions
285  catch {grab release $w}
286  destroy $w
287 
288  set isNewSoundFolder 0
289  if {$soundFolder != $::utils::sound::soundFolder_temp} {
290  set isNewSoundFolder 1
291  }
292 
293  # Update the user-settable sound variables:
294  foreach v {soundFolder announceNew announceForward announceBack} {
295  set ::utils::sound::$v [set ::utils::sound::${v}_temp]
296  }
297 
298  # If the user selected a different folder to look in, read it
299  # and tell the user how many sound files were found there.
300 
301  if {$isNewSoundFolder && $soundFolder != ""} {
302  set numSoundFiles [::utils::sound::ReadFolder]
303  tk_messageBox -title "Scid: Sound Files" -type ok -icon info \
304  -message "Found $numSoundFiles of [llength $::utils::sound::soundFiles] sound files in $::utils::sound::soundFolder"
305  }
306 }
307 
308 # Read the sound files at startup: