DIRECTORY BasicTime USING [GetClockPulses, PulsesToMicroseconds, MicrosecondsToPulses, Pulses], Jukebox USING [bytesPerMS], Process USING [Pause, MsecToTicks, Priority, priorityRealTime, SetPriority, Detach], Rope USING [ROPE, Length], Menus USING [MenuProc], MessageWindow USING [Append, Blink], TiogaOps USING [GetRope, SaveSelA, RestoreSelA, SetSelection, AddLooks, SubtractLooks, ViewerDoc, FirstChild, SelectDocument, SetStyle, SetLooks, GetSelection, SelectPoint, ClearLooks, CancelSelection], TiogaOpsDefs USING [Ref, Location, SelectionGrain], VoiceAging USING [AgeAllViewers, ReColorViewer], VoiceViewers USING [VoiceViewerInfo, SetVoiceViewerEditStatus, soundRopeCharLength, soundRopeCharsPerSecond], VoiceInText USING [thrushHandle, PlaySelection], ViewerClasses USING [Viewer], ViewerOps USING [FetchProp], ViewerTools USING [TiogaContents, TiogaContentsRec, SetTiogaContents], VoiceMarkers USING [RedrawTextMarkers], VoiceRecord USING [RecordingInProgress], VoiceRope USING [VoiceRope, VoiceRopeInterval, Play, Length], VoicePlayBack; VoicePlayBackImpl: CEDAR MONITOR IMPORTS BasicTime, MessageWindow, Process, Rope, TiogaOps, VoiceAging, VoiceViewers, VoiceInText, ViewerOps, ViewerTools, VoiceMarkers, VoiceRecord, VoiceRope EXPORTS VoicePlayBack = BEGIN playBackState: RECORD [ running: BOOLEAN _ FALSE, -- is there currently a forked instance of PlayBackProcess ? queue: LIST OF PlayBackRequest _ 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 [ display: BOOLEAN, -- if not set then there is no displaying to be done viewer: ViewerClasses.Viewer, node: TiogaOpsDefs.Ref, -- the text node is assumed to contain of rope of characters in the start: INT, -- range [start..end]: this is not checked by PlayBackProcess end: INT, -- except that if display=FALSE then viewer, node are unused and these currentPos: INT, -- integers are used only for counting timeRemnant: INT -- (end-start+1) represents the duration of the rope section in 'whole characters' rounded down: this is the rounding error in voice samples ]; cuePriority: Process.Priority = Process.priorityRealTime; aVeryLongTime: BasicTime.Pulses = BasicTime.MicrosecondsToPulses[300000000]; 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 -- so as to cope properly with wrap-around THEN Process.Pause[Process.MsecToTicks[BasicTime.PulsesToMicroseconds[ nextWakeUp-now]/1000]]; [moreToDo, nextWakeUp] _ PlayBackNext[] ENDLOOP }; PlayBackNext: ENTRY PROC RETURNS [moreToDo: BOOLEAN _ TRUE, nextWakeUp: BasicTime.Pulses] = { currRequest: PlayBackRequest _ playBackState.queue.first; IF playBackState.abort THEN { IF currRequest.display AND currRequest.currentPos>=currRequest.start THEN { TiogaOps.SaveSelA[]; TiogaOps.SetSelection[currRequest.viewer, [currRequest.node, MAX[currRequest.start, currRequest.currentPos-1]], [currRequest.node, MIN[currRequest.end, currRequest.currentPos]]]; TiogaOps.AddLooks["v"]; TiogaOps.SubtractLooks["w"]; TiogaOps.RestoreSelA[] }; -- all that restored any W looks to V looks playBackState.queue _ NIL; playBackState.running _ FALSE; playBackState.abort _ FALSE; VoiceViewers.SetVoiceViewerEditStatus[currRequest.viewer]; BROADCAST abortCleared; RETURN [moreToDo: FALSE, nextWakeUp: 0] -- the latter only to satisfy the compiler }; currRequest.currentPos _ currRequest.currentPos + 1; IF currRequest.display THEN { TiogaOps.SaveSelA[]; IF currRequest.currentPos-2 >= currRequest.start THEN { TiogaOps.SetSelection[currRequest.viewer, [currRequest.node, currRequest.currentPos-2], [currRequest.node, currRequest.currentPos-2]]; TiogaOps.AddLooks["v"]; TiogaOps.SubtractLooks["w"] }; IF currRequest.currentPos <= currRequest.end THEN { TiogaOps.SetSelection[currRequest.viewer, [currRequest.node, currRequest.currentPos], [currRequest.node, currRequest.currentPos]]; TiogaOps.AddLooks["w"]; TiogaOps.SubtractLooks["v"] }; TiogaOps.RestoreSelA[] }; IF currRequest.currentPos < currRequest.end + 2 THEN { playBackState.nextTime _ playBackState.nextTime + BasicTime.MicrosecondsToPulses[(IF currRequest.currentPos = currRequest.end + 1 THEN currRequest.timeRemnant*(1000/Jukebox.bytesPerMS) ELSE 1000000/VoiceViewers.soundRopeCharsPerSecond)]; RETURN[nextWakeUp: playBackState.nextTime] }; VoiceViewers.SetVoiceViewerEditStatus[currRequest.viewer]; playBackState.queue _ playBackState.queue.rest; IF playBackState.queue = NIL THEN { playBackState.running _ FALSE; RETURN[moreToDo: FALSE, nextWakeUp: 0] } ELSE RETURN [nextWakeUp: playBackState.nextTime] -- will cause PlayBackNext to be called again immediately to colour the first cell of the next sound section }; QueuePlayBackCue: ENTRY PROC [request: PlayBackRequest] = { WHILE playBackState.running AND playBackState.abort DO WAIT abortCleared ENDLOOP; IF playBackState.running THEN AppendRequest[request] ELSE { p: PROCESS; playBackState.queue _ CONS[request, NIL]; playBackState.nextTime _ BasicTime.GetClockPulses[]; playBackState.running _ TRUE; p _ FORK PlayBackProcess[]; TRUSTED {Process.Detach[p]}; } }; 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: ENTRY PROC = { IF playBackState.running THEN playBackState.abort _ TRUE }; PlayBackMenuProc: PUBLIC Menus.MenuProc = { IF VoiceRecord.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 -- no selection THEN 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 VoiceInText.PlaySelection[] } } }; PlaySlabSection: PUBLIC PROC [viewer: ViewerClasses.Viewer, node: TiogaOpsDefs.Ref, from, to: INT] = { viewerInfo: VoiceViewers.VoiceViewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], VoiceViewers.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 _ [display: TRUE, viewer: viewer, node: node, start: from, end: to, currentPos: from-1, timeRemnant: 0]]; VoiceRope.Play[handle: VoiceInText.thrushHandle, voiceRope: NEW [VoiceRope.VoiceRopeInterval _ [viewerInfo.ropeInterval.ropeID, from*VoiceViewers.soundRopeCharLength, (to-from)*VoiceViewers.soundRopeCharLength]]]; QueuePlayBackCue[newRequest] }; PlayWholeSlab: PUBLIC PROC [viewer: ViewerClasses.Viewer] = { viewerInfo: VoiceViewers.VoiceViewerInfo _ NARROW[ViewerOps.FetchProp[viewer, $voiceViewerInfo], VoiceViewers.VoiceViewerInfo]; IF viewerInfo = NIL THEN MessageWindow.Append["Not a voice window or no selection", TRUE] ELSE { node: TiogaOpsDefs.Ref _ TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]]; nodeLength: INT _ TiogaOps.GetRope[node].Length; newRequest: PlayBackRequest _ NEW[PlayBackRequestRec _ [display: TRUE, viewer: viewer, node: node, start: 0, end: nodeLength-1, currentPos: -1, timeRemnant: viewerInfo.remnant]]; VoiceRope.Play[handle: VoiceInText.thrushHandle, voiceRope: NEW [VoiceRope.VoiceRopeInterval _ viewerInfo.ropeInterval]]; QueuePlayBackCue[newRequest] } }; PlayRopeWithoutCue: PUBLIC PROC [voiceID: Rope.ROPE] = { fullRope: VoiceRope.VoiceRope _ NEW [VoiceRope.VoiceRopeInterval _ [voiceID, 0, 0]]; newRequest: PlayBackRequest; fullRope.length _ VoiceRope.Length[handle: VoiceInText.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 _ [display: FALSE, start: 0, end: fullRope.length/VoiceViewers.soundRopeCharLength, currentPos: -1, timeRemnant: fullRope.length MOD VoiceViewers.soundRopeCharLength]]; VoiceRope.Play[handle: VoiceInText.thrushHandle, voiceRope: fullRope]; QueuePlayBackCue[newRequest] }; PlayBackInProgress: PUBLIC PROC RETURNS [BOOLEAN] = { RETURN [playBackState.running AND ~playBackState.abort] }; -- only a hint: a new playback request may be queued up and wating for the abort to clear before becoming the new playBackState.queue CancelPlayBack: PUBLIC PROC = { AbortCues[] }; 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: VoicePlayBack.SelectionsAfterRedraw] RETURNS [newNode: TiogaOpsDefs.Ref] = { pViewer, sViewer: ViewerClasses.Viewer; pStart, pEnd, sStart, sEnd: TiogaOpsDefs.Location; pLevel, sLevel: TiogaOpsDefs.SelectionGrain; pCaretBefore, sCaretBefore, pPendingDelete, sPendingDelete: BOOLEAN; viewercontents: ViewerTools.TiogaContents _ NEW[ViewerTools.TiogaContentsRec _ []]; [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]; viewercontents.contents _ newContents; ViewerTools.SetTiogaContents[viewer, viewercontents]; TiogaOps.SaveSelA[]; TiogaOps.SelectDocument[viewer]; TiogaOps.SetStyle["voiceProfile", root]; TiogaOps.ClearLooks[]; TiogaOps.SetLooks["v"]; TiogaOps.RestoreSelA[]; newNode _ TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]]; VoiceMarkers.RedrawTextMarkers[viewer, newNode]; 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 VoiceAging.AgeAllViewers[viewer] ELSE VoiceAging.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 _ [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 _ [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 _ [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 { oldCurrentPos: INT _ currRequest.currentPos; newRequests: LIST OF PlayBackRequest _ playBackState.queue.rest; currRequest.node _ newNode; IF currRequest.end >= unchangedHead -- if not, unaffected by the edit [but still will need to put back the playback looks] THEN { -- one advantage of going backwards through these is that it's easy to stop when we find the one currently being played back 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 _ [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 _ [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 _ [display: TRUE, viewer: viewer, node: newNode, start: start, end: end, currentPos: currentPos, timeRemnant: 0]], newRequests] }; playBackState.queue _ newRequests }; IF newRequests.first.display AND newRequests.first.currentPos>=currRequest.start THEN { TiogaOps.SaveSelA[]; TiogaOps.SetSelection[newRequests.first.viewer, [newRequests.first.node, MAX[newRequests.first.start, newRequests.first.currentPos-1]], [newRequests.first.node, MIN[newRequests.first.end, newRequests.first.currentPos]]]; TiogaOps.AddLooks["w"]; TiogaOps.SubtractLooks["v"]; TiogaOps.RestoreSelA[] } } } }; END. œVoicePlayBackImpl.mesa routines replay sounds, plus all the guff to move a cue along a slab display Ades, September 24, 1986 10:21:05 am PDT see the head of VoicePlayBack.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šœœœ˜/šœœ˜!šœ œŸ5˜JKšœ˜KšœŸC˜[KšœœŸ=˜IKšœœŸF˜PKšœ œŸ&˜7Kšœ œŸŒ˜—K˜K˜—KšœK™KKšœ/™/Kšœ™šœè™èKšœ˜—Kšœ9˜9KšœL˜LKšœ¼™¼K˜Kšžœœ˜Kšœ œ˜Kšœ˜˜Kšœ"˜"Kšœˆ™ˆKšœ(˜(K˜šœ ˜Kšœ3˜3Kšœ"Ÿ*˜NK˜KšœZ˜^Kšœ'˜'—Kš˜—K˜K˜š ž œœœœ œœ#˜]Kšœ9˜9—˜Kšœœ˜šœœœ+˜Lšœ˜Kšœ=œCœ,˜²Kšœ˜Kšœ˜Kšœ˜—KšœŸ+˜/Kšœœ˜Kšœœ˜Kšœœ˜Kšœ:˜:Kš œ˜Kšœ œŸ*˜R—K˜K˜Kšœ4˜4K˜Kšœ˜˜Kšœ/˜5šœ‰˜‰Kšœ˜Kšœ˜—K˜Kšœ+˜1šœ…˜…Kšœ˜Kšœ˜—K˜Kšœ˜—Kšœ˜K˜Kšœ-˜/Kšœ˜šœUœ.œ3œ0˜ðKšœ$˜*—K˜K˜K™ºKšœ:˜:Kšœ/˜/Kšœœ˜!šœœ˜!Kšœ œ˜&—Kšœ˜Kšœœ&Ÿl˜—Kšœ˜K˜Kšœ˜šžœœœ˜;Kš œœœœœ˜QKšœ˜Kšœ˜Kš˜šœœ˜Kšœœ œ˜)Kšœ4˜4Kšœœ˜Kšœœ˜Jšœ˜—K˜—K˜K˜šž œœœ˜;Jšœœœ'˜œ ˜ˆKšœÂ œD™‘K™n™ªK˜K™@Kšœ'˜'K˜2Kšœ,˜,Kšœ<œ˜DK˜Kšœ,œ%˜TKšœ–˜–Kšœ˜˜˜K˜Kšœ&˜&Kšœ5˜5K˜Jšœ ˜ Jšœ(˜(Jšœ˜Jšœ˜Jšœ˜Kšœ:˜:Kšœ0˜0Kšœ¾™¾K™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šœœœ"œ"˜gK™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šœœœ˜"šœ<˜Kšœ!˜!—K˜K˜K™YKšœœ1˜Ušœ˜KšœIœUœ8˜ÜKšœ˜Kšœ˜Kšœ˜—K˜—K˜—K˜—˜˜K˜—K˜—šœ˜J˜—Jšœ˜—…—E c