DIRECTORY BasicTime USING [GetClockPulses, MicrosecondsToPulses, Pulses, PulsesToMicroseconds ], Menus USING [MenuProc ], MessageWindow USING [Append, Blink ], Process USING [Detach, MsecToTicks, Priority, priorityRealTime, SetPriority, SetTimeout, Ticks ], Rope USING [Length, ROPE ], TiogaOps USING [CancelSelection, FirstChild, GetRope, GetSelection, SelectionGrain, SelectPoint, SetSelection, ViewerDoc ], TiogaOpsDefs USING [Location, Ref, SelectionGrain ], TiogaVoicePrivate USING [ AgeAllViewers, PlayBackRequestState, PlaySelection, ReColorViewer, RecordingInProgress, SelectionsAfterRedraw, SetTiogaViewerParams, SetVoiceLooks, SetVoiceViewerEditStatus, SoundList, soundRopeCharLength, soundRopeCharsPerSecond, thrushHandle, VoiceViewerInfo ], ViewerClasses USING [Viewer ], ViewerOps USING [FetchProp ], VoiceRope USING [Length, NB, NewRequestID, nullRequestID, PlayNB, RequestID, VoiceRope, VoiceRopeInterval ] ; VoicePlaybackImpl: CEDAR MONITOR IMPORTS BasicTime, MessageWindow, Process, Rope, TiogaOps, TiogaVoicePrivate, ViewerOps, VoiceRope EXPORTS TiogaVoicePrivate = { PlayBackRequestList: TYPE = LIST OF PlayBackRequest; playBackState: RECORD [ running: BOOLEAN _ FALSE, -- is there currently a forked instance of PlayBackProcess ? queue: PlayBackRequestList _ NIL, nextTime: BasicTime.Pulses, -- time to wake up playBackProcess next abort: BOOLEAN _ FALSE -- set when a 'stop voice playback' button is hit - playBackProcess will destroy itself next time it wakes up ]; abortCleared: CONDITION; -- signalled by PlayBackProcess when it has acted on an abort PlayBackRequest: TYPE = REF PlayBackRequestRec; PlayBackRequestRec: TYPE = RECORD[ requestID: VoiceRope.RequestID _ VoiceRope.nullRequestID, state: TiogaVoicePrivate.PlayBackRequestState _ waiting, expectFinishedReport: BOOLEAN _ TRUE, -- if FALSE, then this entry was edited after it was queued, so go on to the next entry automatically. See RedrawViewer. display: BOOLEAN _ TRUE, -- if not set then there is no displaying to be done viewer: ViewerClasses.Viewer _ NIL, node: TiogaOpsDefs.Ref, -- the text node is assumed to contain of rope of characters in the start: INT _ 0, -- range [start..end]: this is not checked by PlayBackProcess end: INT _ 0, -- except that if display=FALSE then viewer, node are unused and these currentPos: INT _ -1, -- integers are used only for counting startTime: BasicTime.Pulses _ 0, -- when playback was started timeRemnant: INT _ 0 -- (end-start+1) represents the duration of the rope section in 'whole characters' rounded down: this is the rounding error in voice samples (OBSOLETE) ]; cuePriority: Process.Priority = Process.priorityRealTime; aVeryLongTime: BasicTime.Pulses = BasicTime.MicrosecondsToPulses[300000000]; oneSoundCharTime: BasicTime.Pulses = BasicTime.MicrosecondsToPulses[1000000/TiogaVoicePrivate.soundRopeCharsPerSecond]; waitForReportTime: BasicTime.Pulses = BasicTime.MicrosecondsToPulses[100000000]; -- 100 sec PlayBackProcess: PROC = { moreToDo: BOOLEAN; nextWakeUp: BasicTime.Pulses; Process.SetPriority[cuePriority]; [moreToDo, nextWakeUp] _ PlayBackNext[]; WHILE moreToDo DO now: BasicTime.Pulses _ BasicTime.GetClockPulses[]; IF nextWakeUp - now < aVeryLongTime THEN { -- so as to cope properly with wrap-around timeout: Process.Ticks _ Process.MsecToTicks[BasicTime.PulsesToMicroseconds[ nextWakeUp-now]/1000]; TRUSTED { Process.SetTimeout[@playBackCue, timeout]; }; Wait[]; }; [moreToDo, nextWakeUp] _ PlayBackNext[]; ENDLOOP; }; playBackCue: CONDITION; Wait: ENTRY PROC [] RETURNS [] ~ { WAIT playBackCue; }; PlayBackNext: ENTRY PROC RETURNS [moreToDo: BOOLEAN _ TRUE, nextWakeUp: BasicTime.Pulses] = { currRequest: PlayBackRequest _ playBackState.queue.first; IF playBackState.abort THEN { RemoveCue[currRequest]; playBackState.queue _ NIL; playBackState.running _ FALSE; playBackState.abort _ FALSE; TiogaVoicePrivate.SetVoiceViewerEditStatus[currRequest.viewer]; BROADCAST abortCleared; RETURN [moreToDo: FALSE, nextWakeUp: 0] -- the latter only to satisfy the compiler }; WHILE currRequest.state = done OR (NOT currRequest.expectFinishedReport AND currRequest.currentPos>currRequest.end) DO prevRequest: PlayBackRequest; RemoveCue[currRequest]; TiogaVoicePrivate.SetVoiceViewerEditStatus[currRequest.viewer]; playBackState.queue _ playBackState.queue.rest; IF playBackState.queue = NIL THEN { playBackState.running _ FALSE; RETURN[moreToDo: FALSE, nextWakeUp: 0]; }; prevRequest _ currRequest; currRequest _ playBackState.queue.first; IF NOT prevRequest.expectFinishedReport THEN IF currRequest.start > currRequest.end THEN currRequest.state _ done ELSE {currRequest.startTime _ BasicTime.GetClockPulses[]; currRequest.state _ busy}; ENDLOOP; IF currRequest.state = busy AND (currRequest.display OR NOT currRequest.expectFinishedReport) THEN { currRequest.currentPos _ currRequest.currentPos + 1; IF currRequest.display THEN { IF currRequest.currentPos-2 >= currRequest.start THEN TiogaVoicePrivate.SetVoiceLooks[currRequest.node, currRequest.currentPos-2, 1, "v"]; IF currRequest.currentPos <= currRequest.end THEN TiogaVoicePrivate.SetVoiceLooks[currRequest.node, currRequest.currentPos, 1, "w"]; }; playBackState.nextTime _ currRequest.startTime + (currRequest.currentPos-currRequest.start+1)*oneSoundCharTime; } ELSE playBackState.nextTime _ playBackState.nextTime + waitForReportTime; RETURN[moreToDo: TRUE, nextWakeUp: playBackState.nextTime] }; RemoveCue: INTERNAL PROC [request: PlayBackRequest] ~ { IF NOT request.display THEN RETURN; IF request.start <= request.currentPos AND request.currentPos-1 <= request.end THEN TiogaVoicePrivate.SetVoiceLooks[request.node, request.currentPos-1, 2, "v"]; }; QueuePlayBackCue: ENTRY PROC [request: PlayBackRequest] = { WHILE playBackState.running AND playBackState.abort DO WAIT abortCleared ENDLOOP; IF playBackState.running THEN AppendRequest[request] ELSE { playBackState.queue _ CONS[request, NIL]; playBackState.nextTime _ BasicTime.GetClockPulses[]; playBackState.running _ TRUE; TRUSTED {Process.Detach[FORK PlayBackProcess[]]}; }; }; AppendRequest: INTERNAL PROC [request: PlayBackRequest] = { hangOffPoint: LIST OF PlayBackRequest _ playBackState.queue; oneElementList: LIST OF PlayBackRequest _ CONS[request, NIL]; WHILE hangOffPoint.rest # NIL DO hangOffPoint _ hangOffPoint.rest ENDLOOP; hangOffPoint.rest _ oneElementList }; AbortCues: INTERNAL PROC = { IF playBackState.running THEN playBackState.abort _ TRUE }; PlayBackMenuProc: PUBLIC Menus.MenuProc = { IF TiogaVoicePrivate.RecordingInProgress[] THEN { MessageWindow.Append["Stop recording before trying to play back", TRUE]; MessageWindow.Blink[]; RETURN }; IF mouseButton = blue -- i.e. right THEN PlayWholeSlab[NARROW[parent, ViewerClasses.Viewer]] ELSE { viewer: ViewerClasses.Viewer; start, end: TiogaOpsDefs.Location; [viewer: viewer, start: start, end: end] _ TiogaOps.GetSelection[]; IF viewer = NIL THEN -- no selection PlayWholeSlab[NARROW[parent, ViewerClasses.Viewer]] ELSE { IF ViewerOps.FetchProp[viewer, $voiceViewerInfo] # NIL THEN { IF start.node # end.node THEN ERROR; -- voice viewers only have one display node !!!! TiogaOps.SelectPoint[viewer, start]; -- leave caret at left of playback selection PlaySlabSection[viewer, start.node, start.where, end.where] } ELSE TiogaVoicePrivate.PlaySelection[] } } }; PlaySlabSection: PUBLIC PROC [viewer: ViewerClasses.Viewer, node: TiogaOpsDefs.Ref, from, to: INT] = { nb: VoiceRope.NB; viewerInfo: TiogaVoicePrivate.VoiceViewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; newRequest: PlayBackRequest; IF viewerInfo = NIL THEN RETURN; -- somebody's buggering around with the selection IF node # TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]] THEN ERROR; newRequest _ NEW[PlayBackRequestRec _ [requestID: VoiceRope.NewRequestID[], viewer: viewer, node: node, start: from, end: to, currentPos: from-1]]; QueuePlayBackCue[newRequest]; [nb: nb] _ VoiceRope.PlayNB[handle: TiogaVoicePrivate.thrushHandle, voiceRope: NEW [VoiceRope.VoiceRopeInterval _ [viewerInfo.ropeInterval.ropeID, from*TiogaVoicePrivate.soundRopeCharLength, (to-from)*TiogaVoicePrivate.soundRopeCharLength]], requestID: newRequest.requestID, clientData: $TiogaVoice]; IF nb # $success THEN SetPlayBackState[newRequest.requestID, done]; }; PlayWholeSlab: PUBLIC PROC [viewer: ViewerClasses.Viewer] = { viewerInfo: TiogaVoicePrivate.VoiceViewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], TiogaVoicePrivate.VoiceViewerInfo]; IF viewerInfo = NIL THEN MessageWindow.Append["Not a voice window or no selection", TRUE] ELSE { nb: VoiceRope.NB; node: TiogaOpsDefs.Ref _ TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]]; nodeLength: INT _ TiogaOps.GetRope[node].Length; newRequest: PlayBackRequest _ NEW[PlayBackRequestRec _ [requestID: VoiceRope.NewRequestID[], viewer: viewer, node: node, end: nodeLength-1, timeRemnant: viewerInfo.remnant]]; QueuePlayBackCue[newRequest]; [nb: nb] _ VoiceRope.PlayNB[handle: TiogaVoicePrivate.thrushHandle, voiceRope: NEW [VoiceRope.VoiceRopeInterval _ viewerInfo.ropeInterval], requestID: newRequest.requestID, clientData: $TiogaVoice]; IF nb # $success THEN SetPlayBackState[newRequest.requestID, done]; } }; PlayRopeWithoutCue: PUBLIC PROC [voiceID: Rope.ROPE] = { nb: VoiceRope.NB; fullRope: VoiceRope.VoiceRope _ NEW [VoiceRope.VoiceRopeInterval _ [voiceID, 0, 0]]; newRequest: PlayBackRequest; fullRope.length _ VoiceRope.Length[handle: TiogaVoicePrivate.thrushHandle, vr: fullRope]; IF fullRope.length = 0 THEN RETURN; IF fullRope.length = -1 THEN { MessageWindow.Append["non-existant voice utterance(s) found in selection", TRUE]; RETURN }; newRequest _ NEW [PlayBackRequestRec _ [requestID: VoiceRope.NewRequestID[], display: FALSE, end: fullRope.length/TiogaVoicePrivate.soundRopeCharLength, timeRemnant: fullRope.length MOD TiogaVoicePrivate.soundRopeCharLength]]; QueuePlayBackCue[newRequest]; [nb: nb] _ VoiceRope.PlayNB[handle: TiogaVoicePrivate.thrushHandle, voiceRope: fullRope, requestID: newRequest.requestID, clientData: $TiogaVoice]; IF nb # $success THEN SetPlayBackState[newRequest.requestID, done]; }; PlayBackInProgress: PUBLIC PROC RETURNS [BOOLEAN] = { RETURN [playBackState.running AND ~playBackState.abort] }; -- only a hint: a new playback request may be queued up and waiting for the abort to clear before becoming the new playBackState.queue CancelPlayBack: PUBLIC ENTRY PROC = { AbortCues[] }; GetCurrentPlayBackPos: PUBLIC PROC RETURNS [display: BOOLEAN_FALSE, viewer: ViewerClasses.Viewer_NIL, currentPos: INT_-1] = { IF playBackState.queue#NIL THEN { currRequest: PlayBackRequest _ playBackState.queue.first; IF currRequest.state=busy THEN RETURN[currRequest.display, currRequest.viewer, currRequest.currentPos]; }; }; SetPlayBackState: PUBLIC ENTRY PROC [reqID: VoiceRope.RequestID, newState: TiogaVoicePrivate.PlayBackRequestState] = { FOR playQ: PlayBackRequestList _ playBackState.queue, playQ.rest WHILE playQ#NIL DO IF playQ.first.requestID = reqID THEN { playQ.first.state _ newState; -- worry about busyi after donei ? IF newState=busy THEN playQ.first.startTime _ BasicTime.GetClockPulses[]; IF playQ#playBackState.queue THEN FOR pQ: PlayBackRequestList _ playBackState.queue, pQ.rest WHILE pQ#playQ DO pQ.first.state _ done; -- must have missed the report ENDLOOP; BROADCAST playBackCue; RETURN; }; ENDLOOP; }; RemoveViewerReferences: PUBLIC ENTRY PROC [viewer: ViewerClasses.Viewer] RETURNS [okay: BOOLEAN _ TRUE] = { IF playBackState.queue = NIL THEN RETURN; IF playBackState.queue.first.display AND playBackState.queue.first.viewer = viewer THEN RETURN [FALSE]; FOR l: LIST OF PlayBackRequest _ playBackState.queue.rest, l.rest WHILE l # NIL DO IF l.first.viewer = viewer THEN l.first.display _ FALSE ENDLOOP }; RedrawViewer: PUBLIC ENTRY PROC [viewer: ViewerClasses.Viewer, newContents: Rope.ROPE, unchangedHead, deleteChars, insertChars: INT, timeRemnant: INT, age: BOOLEAN, selectionsAfterRedraw: TiogaVoicePrivate.SelectionsAfterRedraw] RETURNS [newNode: TiogaOpsDefs.Ref] = { pViewer, sViewer: ViewerClasses.Viewer; pStart, pEnd, sStart, sEnd: TiogaOpsDefs.Location; pLevel, sLevel: TiogaOpsDefs.SelectionGrain; pCaretBefore, sCaretBefore, pPendingDelete, sPendingDelete: BOOLEAN; [viewer: pViewer, start: pStart, end: pEnd, level: pLevel, caretBefore: pCaretBefore, pendingDelete: pPendingDelete] _ TiogaOps.GetSelection[primary]; [viewer: sViewer, start: sStart, end: sEnd, level: sLevel, caretBefore: sCaretBefore, pendingDelete: sPendingDelete] _ TiogaOps.GetSelection[secondary]; newNode _ TiogaVoicePrivate.SetTiogaViewerParams[viewer, newContents]; SELECT selectionsAfterRedraw FROM unAltered => -- if the selected character have been edited then we'll try to keep the selection on the same bits of voice { lastCharInNode: INT _ (TiogaOps.GetRope[newNode]).Length - 1; IF pViewer = viewer THEN { IF pStart.node # pEnd.node THEN TiogaOps.CancelSelection[primary] ELSE { pStart.node _ pEnd.node _ newNode; IF pStart.where >= unchangedHead THEN pStart.where _ pStart.where - deleteChars + insertChars; IF pEnd.where >= unchangedHead THEN pEnd.where _ pEnd.where - deleteChars + insertChars; IF pStart.where > lastCharInNode THEN pStart.where _ lastCharInNode; IF pEnd.where > lastCharInNode THEN pEnd.where _ lastCharInNode; TiogaOps.SetSelection[viewer: pViewer, start: pStart, end: pEnd, level: pLevel, caretBefore: pCaretBefore, pendingDelete: pPendingDelete, which: primary] } }; IF sViewer = viewer THEN { IF sStart.node # sEnd.node THEN TiogaOps.CancelSelection[secondary] ELSE { sStart.node _ sEnd.node _ newNode; IF sStart.where >= unchangedHead THEN sStart.where _ sStart.where - deleteChars + insertChars; IF sEnd.where >= unchangedHead THEN sEnd.where _ sEnd.where - deleteChars + insertChars; IF sStart.where > lastCharInNode THEN sStart.where _ lastCharInNode; IF sEnd.where > lastCharInNode THEN sEnd.where _ lastCharInNode; TiogaOps.SetSelection[viewer: sViewer, start: sStart, end: sEnd, level: sLevel, caretBefore: sCaretBefore, pendingDelete: sPendingDelete, which: secondary] } } }; deSelected => { IF pViewer = viewer THEN TiogaOps.CancelSelection[primary]; IF sViewer = viewer THEN TiogaOps.CancelSelection[secondary] }; primaryOnInsert => { nodeLength: INT _ (TiogaOps.GetRope[newNode]).Length; IF sViewer = viewer THEN TiogaOps.CancelSelection[secondary]; IF nodeLength = 0 THEN TiogaOps.CancelSelection[primary] -- in case the viewer is empty ELSE { pViewer _ viewer; pStart.node _ pEnd.node _ newNode; IF insertChars = 0 THEN -- leave a point selection { pStart.where _ pEnd.where _ (IF unchangedHead > nodeLength THEN nodeLength ELSE unchangedHead); pLevel _ point } ELSE { pStart.where _ unchangedHead; pEnd.where _ unchangedHead + insertChars -1; pLevel _ char }; pCaretBefore _ pPendingDelete _ FALSE; TiogaOps.SetSelection[viewer: pViewer, start: pStart, end: pEnd, level: pLevel, caretBefore: pCaretBefore, pendingDelete: pPendingDelete, which: primary] } }; ENDCASE => ERROR; IF age AND insertChars # 0 THEN TiogaVoicePrivate.AgeAllViewers[viewer] ELSE TiogaVoicePrivate.ReColorViewer[viewer]; IF playBackState.queue # NIL THEN FOR p: LIST OF PlayBackRequest _ playBackState.queue, p.rest WHILE p.rest # NIL DO currRequest: PlayBackRequest _ p.rest.first; IF currRequest.display AND currRequest.viewer = viewer THEN { currRequest.node _ newNode; -- I've just changed it under your feet!! [okay - monitored] IF currRequest.end >= unchangedHead -- if not, unaffected by the edit THEN { newRequests: LIST OF PlayBackRequest _ p.rest.rest; IF currRequest.end >= unchangedHead + deleteChars THEN -- non-empty third part { start: INT _ MAX[ unchangedHead + deleteChars, currRequest.start ] + (insertChars - deleteChars); end: INT _ currRequest.end + (insertChars - deleteChars); newRequests _ CONS[ NEW[ PlayBackRequestRec _ [requestID: currRequest.requestID, state: waiting, expectFinishedReport: TRUE, display: TRUE, viewer: viewer, node: newNode, start: start, end: end, currentPos: start-1, timeRemnant: timeRemnant]], newRequests] }; IF currRequest.start < unchangedHead + deleteChars THEN -- non-empty second part [since request cannot lie entirely in first part - see two IFs back] { start: INT _ MAX[unchangedHead, currRequest.start]; end: INT _ MIN [unchangedHead + deleteChars - 1, currRequest.end]; newRequests _ CONS[ NEW[ PlayBackRequestRec _ [requestID: currRequest.requestID, state: waiting, expectFinishedReport: FALSE, display: FALSE, start: start, end: end, currentPos: start-1, timeRemnant: 0]], newRequests] }; IF currRequest.start < unchangedHead THEN -- non-empty first part { start: INT _ currRequest.start; end: INT _ MIN [unchangedHead - 1, currRequest.end]; newRequests _ CONS[ NEW[ PlayBackRequestRec _ [requestID: currRequest.requestID, state: waiting, expectFinishedReport: FALSE, display: TRUE, viewer: viewer, node: newNode, start: start, end: end, currentPos: start-1, timeRemnant: 0]], newRequests] }; p.rest _ newRequests } } ENDLOOP; IF playBackState.queue # NIL THEN { currRequest: PlayBackRequest _ playBackState.queue.first; currentPosFound: BOOLEAN _ FALSE; IF currRequest.display AND currRequest.viewer = viewer THEN { newRequests: LIST OF PlayBackRequest _ playBackState.queue.rest; currRequest.node _ newNode; IF currRequest.end >= unchangedHead THEN { IF currRequest.end >= unchangedHead + deleteChars THEN { start: INT _ MAX[ unchangedHead + deleteChars, currRequest.start ] + (insertChars - deleteChars); end: INT _ currRequest.end + (insertChars - deleteChars); currentPos: INT _ MAX [ start-1, currRequest.currentPos + (insertChars - deleteChars) ]; newRequests _ CONS[ NEW[ PlayBackRequestRec _ [requestID: currRequest.requestID, state: waiting, expectFinishedReport: TRUE, display: TRUE, viewer: viewer, node: newNode, start: start, end: end, currentPos: currentPos, timeRemnant: timeRemnant]], newRequests]; IF currentPos # start-1 THEN currentPosFound _ TRUE; }; IF (~currentPosFound) AND currRequest.start < unchangedHead + deleteChars THEN { start: INT _ MAX[unchangedHead, currRequest.start]; end: INT _ MIN [unchangedHead + deleteChars - 1, currRequest.end]; currentPos: INT _ MAX [ start-1, currRequest.currentPos]; -- no need to put in a MIN clause since otherwise currentPosFound=TRUE and we'd never have got here newRequests _ CONS[ NEW[ PlayBackRequestRec _ [requestID: currRequest.requestID, state: waiting, expectFinishedReport: FALSE, display: FALSE, viewer: viewer, node: newNode, start: start, end: end, currentPos: currentPos, timeRemnant: 0]], newRequests]; IF currentPos # start-1 THEN currentPosFound _ TRUE; }; IF (~currentPosFound) AND currRequest.start < unchangedHead THEN { start: INT _ currRequest.start; end: INT _ MIN [unchangedHead - 1, currRequest.end]; currentPos: INT _ currRequest.currentPos; newRequests _ CONS[ NEW[ PlayBackRequestRec _ [requestID: currRequest.requestID, state: currRequest.state, expectFinishedReport: FALSE, display: TRUE, viewer: viewer, node: newNode, start: start, end: end, currentPos: currentPos, timeRemnant: 0]], newRequests] }; newRequests.first.state _ currRequest.state; newRequests.first.startTime _ currRequest.startTime; playBackState.queue _ newRequests; currRequest _ playBackState.queue.first; }; IF currRequest.display AND currRequest.currentPos>=currRequest.start THEN { start: INT _ MAX[currRequest.start, currRequest.currentPos-1]; len: INT _ MIN[currRequest.end, currRequest.currentPos] - start; TiogaVoicePrivate.SetVoiceLooks[node: currRequest.node, start: start, len: len, looks: "w"]; } } } }; }. ÖVoicePlaybackImpl.mesa Copyright Ó 1987 by Xerox Corporation. All rights reserved. Ades, September 24, 1986 5:11:21 pm PDT Swinehart, April 10, 1987 9:38:59 am PDT Polle Zellweger (PTZ) July 27, 1987 1:43:57 pm PDT Routines replay sounds, plus all the guff to move a cue along a slab display see TiogaVoicePrivate.mesa for the raison d'etre of this module when placed in the playback queue, a request should have currentPos=start-1 requests with end˜WK˜—Kšœœœ˜/šœœœ˜"Kšœ9˜9Kšœ8˜8KšœœŸy˜ŸKšœ œœŸ4˜MKšœœ˜#KšœŸC˜[KšœœŸ=˜MKšœœŸF˜TKšœ œŸ&˜œ˜Sšœœœ˜'KšœŸÐcdŸ ¢Ÿ˜AKšœœ4˜Išœœ˜"šœ8œ˜LKšœŸ˜5Kšœ˜——Kš œ ˜Kšœ˜K˜—Kšœ˜—K˜K˜—š œœœœ œœœ˜kKšœœœœ˜)Kš œ#œ+œœœ˜gš œœœ4œœ˜RKšœœ˜7—Kš˜Kšœ˜K˜—š  œœœœ2œ+œœœBœ ˜ŒKšœ‘™‘K™uK™ªK˜K™@Kšœ'˜'K˜2Kšœ,˜,Kšœ<œ˜DK˜Kšœ–˜–Kšœ˜˜˜K˜KšœÏtœ*˜FK˜K™#Kšœ˜!šœŸl˜zšœœ*˜@Kšœ˜šœœœ"˜DKš˜šœ%˜%Kšœœ9˜^Kšœœ5˜XKšœœ˜DKšœœ˜@Kšœ™˜™—K˜—K˜Kšœ˜šœœœ$˜FKš˜šœ%˜%Kšœœ9˜^Kšœœ5˜XKšœœ˜DKšœœ˜@Kšœ›˜›—K˜—K˜—K˜—˜ šœœœ#˜>Kšœœ$˜<—K˜—˜šœœ&˜8Kšœœ%˜=Kšœœ#Ÿ˜XKš˜šœ˜Kšœ"˜"Kšœ˜KšœŸ˜šœ œœ œ˜bKšœ˜—Kšœ˜Kš˜šœ ˜ Kšœ,˜,Kšœ ˜ —K˜Kšœ œ˜&Kšœ™˜™—K˜—Kšœ˜—Kšœœ˜K™Kšœœœ)œ)˜uK™K™Ušœœœœœœ/œ œ˜tKšœ,˜,Kšœœ˜;šœŸ<˜[Kšœ"Ÿ!˜EKšœ˜šœœœ˜6KšœÒ™ÒK™K™”K˜Kšœ0œŸ˜Nšœ œœR˜eKšœœ1˜9Kš œœœ`œ œv˜€—K˜K˜Kšœ1œŸ]˜•šœ œœ$˜7Kšœœœ4˜BKš œœœ`œ œM˜Ù—K˜K˜Kšœ#œŸ˜Ašœ œ˜"Kšœœœ&˜4Kš œœœ`œ œl˜÷—K˜K˜K™=Kšœ˜—K˜—K˜—Kšœ˜K˜K™ªK˜šœœœ˜#Kšœ9˜9Kšœœœ˜!šœœœ˜=Kšœ œœ,˜@Kšœ˜šœ"œ˜*KšœT™TKšœy™yšœ0œ˜8KšœœœR˜bKšœœ1˜9Kšœ œœD˜YKš œœœ`œ œz˜„šœ˜Kšœœ˜—K˜—K˜šœœ1œ˜PKšœœœ$˜4Kšœœœ4˜BKšœ œœ%Ÿc˜Kš œœœ`œ œp˜üšœ˜Kšœœ˜—K˜—K˜šœœ#œ˜BKšœœ˜Kšœœœ&˜4Kšœ œ˜)Kš œœœjœ œo˜„K˜—Kšœ,˜,Kšœ4˜4K™>Kšœ"˜"Kšœ(˜(K˜—K˜K™Yšœœ+œ˜KKšœœœ.˜>Kšœœœ2˜@Kšœ\˜\K˜—K˜—K˜—K˜K˜—J˜J˜™(K™wKšœ Ïr9™E—™1K™-Kšœ ¤jœ¤œ™º—™1K™íKšœ ¤Nœ¤#œ¤™Ï—™1K™i—™2Kšœ ¤œ<™_—™1K™BKšœ ¤™!—™1K™dKšœ ¤™&—™3K™"Kšœ ¤™&—™2K™ÊKšœ ¤˜™¤—™2Kšœ ¤2™>—K™—…—Lˆz