DIRECTORY Commander USING [CommandProc, Register, Handle], CommandTool USING [ArgumentVector, Failed, Parse], Containers USING [Container, Create], Convert USING [RopeFromInt, IntFromRope], Icons USING [NewIconFromFile], IO USING [PutF, PutFR, int, STREAM], Imager USING [Context, MaskRectangle, SetFont, SetColor, SetXY, ShowRope, TranslateT, black, MakeGray], ImagerColorDefs USING [ConstantColor], ImagerFont USING [Find, Font, RopeWidth], Jukebox USING [bytesPerChirp, CloseJukebox, ArchiveCloseTune, Error, FindJukebox, Handle, OpenJukebox, OpenTune, Tune, TuneSize, EnergyRange, instances, singlePktLength, hangoverPackets, MissingChirp, EOF], Rope USING [Cat, ROPE, Equal, Length, Fetch, Substr], Vector2 USING [VEC], ViewerEvents USING [EventProc, RegisterEventProc], ViewerClasses USING [PaintProc, Viewer, ViewerClass, ViewerClassRec], ViewerOps USING [CreateViewer, RegisterViewerClass, SetOpenHeight], TuneAccess USING [EnergyBlock, GetEnergyProfile, ReadAmbientLevel]; TuneGraphsImpl: CEDAR PROGRAM IMPORTS Commander, CommandTool, Containers, Convert, Icons, IO, Imager, ImagerFont, Jukebox, Rope, ViewerEvents, ViewerOps, TuneAccess = { Context: TYPE ~ Imager.Context; VEC: TYPE ~ Vector2.VEC; Font: TYPE ~ ImagerFont.Font; CommandName: TYPE = {plotTune, plotDistribution}; Data: TYPE = REF DataRecord; DataRecord: TYPE = RECORD [ container: Containers.Container _ NIL, graphs: ViewerClasses.Viewer _ NIL, commandName: CommandName, jukeboxName: Rope.ROPE _ NIL, tuneString: Rope.ROPE _ NIL, height: INT, yMax: CARDINAL, rows: INT, width: INT, totalHeight: NAT, points: REF TwoDArrayOfPoints _ NEW [TwoDArrayOfPoints _ ALL[NIL]], helvetica6: Font _ ImagerFont.Find["Xerox/TiogaFonts/Helvetica6"], helvetica8: Font _ ImagerFont.Find["Xerox/TiogaFonts/Helvetica8"], helvetica10: Font _ ImagerFont.Find["Xerox/TiogaFonts/Helvetica10"], tuneNumberFont: Font]; leading: NAT = 2; separation: NAT = 10; buttonHeight: NAT = 15; headerHeight: NAT = 12; left: INT = 50; bottom: INT = leading+headerHeight+leading; top: INT = 10; maxWidth: INT = 500; gray: ImagerColorDefs.ConstantColor = Imager.MakeGray[0.5]; maxRows: INT = 8; -- most that will fit on a screen [assumes a value of data.height - naughty] TwoDArrayOfPoints: TYPE = ARRAY [0..maxRows) OF REF ArrayOfPoints; RealInitialisedToZero: TYPE = REAL _ 0; ArrayOfPoints: TYPE = RECORD [ s: SEQUENCE max: NAT OF RealInitialisedToZero]; Title: PROC [context: Context, data: Data, t1, t2, t3: Rope.ROPE] ~ { Imager.SetFont[context, data.helvetica6]; Imager.SetXY[context, [-left+leading+leading, leading+headerHeight]]; Imager.ShowRope[context, t1]; Imager.SetFont[context, data.tuneNumberFont]; Imager.SetXY[context, [-left+leading+leading, leading]]; Imager.ShowRope[context, t2]; Imager.SetFont[context, data.helvetica6]; Imager.SetXY[context, [-left+leading+leading, 2*leading-headerHeight]]; Imager.ShowRope[context, t3]; }; BasicFrame: PROC [context: Context, data: Data, height: INT] ~ { Imager.SetFont[context, data.helvetica8]; Imager.MaskRectangle[context, [x: -1, y: 0, w: data.width+2, h: -1.0]]; Imager.MaskRectangle[context, [x: 0, y: -1, w: -1.0, h: height+1]]; Imager.MaskRectangle[context, [x: data.width, y: -1, w: 1.0, h: height+1]]; FOR i: INT _ 0, i + 60 UNTIL i > data.width DO Imager.MaskRectangle[context, [x: i, y: -1, w: -1.0, h: -5.0]]; Imager.SetXY[context, [i+leading, 0+leading-headerHeight]]; ENDLOOP; }; Frame: PROC [context: Context, data: Data, max: INT] ~ { BasicFrame[context, data, data.height]; FOR i: INT _ 0, i + 30 UNTIL i > data.height DO value: INT = max*i/data.height; tag: Rope.ROPE = AbbRopeFromInt[value]; ropeWidth: VEC = ImagerFont.RopeWidth[data.helvetica8, tag]; Imager.SetXY[context, [0-ropeWidth.x-5-5, i]]; IF i # 0 THEN Imager.ShowRope[context, tag]; Imager.MaskRectangle[context, [x: -1, y: i, w: -5.0, h: -1.0]]; Imager.MaskRectangle[context, [x: data.width+1, y: i, w: +5.0, h: -1.0]]; Imager.SetXY[context, [0+data.width+5+5, i]]; Imager.ShowRope[context, tag]; ENDLOOP; }; AbbRopeFromInt: PROC [i: INT] RETURNS [rope: Rope.ROPE] = { IF i = 0 THEN RETURN["0"]; SELECT TRUE FROM (i MOD 1000) = 0 => rope _ Rope.Cat[Convert.RopeFromInt[i/1000], "K"]; (i MOD 1000000) = 0 => rope _ Rope.Cat[Convert.RopeFromInt[i/1000000], "M"]; ENDCASE => rope _ Convert.RopeFromInt[i]; }; PlotData: PROC [context: Context, data: Data, what: REF ArrayOfPoints, max: INT, t1, t2, t3: Rope.ROPE] = { scale: REAL = REAL[max]/REAL[data.height]; Imager.TranslateT[context, [0, -(data.height + top)]]; Frame[context, data, max]; FOR i: INT IN [0..data.width) DO sample: REAL _ REAL[what[i]]/scale ; IF sample > 0 THEN Imager.SetColor[context, Imager.black] ELSE {Imager.SetColor[context, gray]; sample _ -sample}; sample _ MIN[sample, REAL[data.height]]; Imager.MaskRectangle[context, [x: i, y: 0, w: 1.0, h: sample]]; ENDLOOP; Imager.SetColor[context, Imager.black]; Title[context, data, t1, t2, t3]; Imager.TranslateT[context, [0, -bottom]]; }; RepaintViewer: ViewerClasses.PaintProc = { data: Data = NARROW[self.data]; Imager.TranslateT[context, [left, data.totalHeight]]; FOR i: INT IN [1..data.rows] DO PlotData[context, data, data.points[i-1], data.yMax, data.jukeboxName, data.tuneString, IF data.commandName = plotDistribution THEN ( IF i = 1 THEN "whole: coarse" ELSE "peak: fine" ) ELSE IO.PutFR["Line %d of %d", IO.int[i], IO.int[data.rows]]]; Imager.MaskRectangle[context, [x: -left, y: 0, w: left+data.width+left, h: 1]] ENDLOOP }; global: Data _ NIL; Poof: ViewerEvents.EventProc = { data: Data = NARROW[viewer.data]; IF global = data THEN global _ NIL; }; AddPoint: PROC [currPlotPoint: INT, data: Data, value: INT] RETURNS [nextPlotPoint: INT] = { IF currPlotPoint<0 THEN currPlotPoint_0; IF currPlotPoint>=data.rows*data.width THEN currPlotPoint_data.rows*data.width-1; data.points[currPlotPoint/data.width][currPlotPoint MOD data.width] _ value; currPlotPoint_currPlotPoint+1; RETURN [IF currPlotPoint>=data.rows*data.width THEN data.rows*data.width-1 ELSE currPlotPoint] }; LastComponentWithoutExtension: PROC [fileName: Rope.ROPE] RETURNS [simpleName: Rope.ROPE] = { start, end: INT _ fileName.Length[] - 1; curr: INT; IF fileName.Fetch[start] = '> OR fileName.Fetch[start] = '/ THEN ERROR; WHILE fileName.Fetch[start-1] # '> AND fileName.Fetch[start-1] # '/ AND start > 0 DO start _ start - 1 ENDLOOP; curr _ start; IF fileName.Fetch[start] = '. OR fileName.Fetch[start] = '! THEN ERROR; WHILE curr < end DO IF fileName.Fetch[curr+1] = '. OR fileName.Fetch[curr+1] = '! THEN end _ curr ELSE curr _ curr + 1 ENDLOOP; RETURN [fileName.Substr[start, end-start+1]] }; ParseTuneProfile: PROC [data: Data, cmd: Commander.Handle] RETURNS [succeeded: BOOLEAN _ FALSE, error: Rope.ROPE _ NIL] = TRUSTED { weOpened: BOOL _ FALSE; jukebox: Jukebox.Handle _ NIL; tune: Jukebox.Tune _ NIL; {{ ENABLE Jukebox.Error => { error _ rope; GOTO Quit }; argv: CommandTool.ArgumentVector; tuneSize: INT; tuneID: INT; sampleIncrement: INT; pointsPerChirp: INT; currPlotPoint: INT _ 0; peakEnergy: Jukebox.EnergyRange _ 0; argv _ CommandTool.Parse[cmd ! CommandTool.Failed => {error _ errorMsg; GOTO Quit}]; IF argv.argc < 3 OR argv.argc > 4 THEN { error _ "Usage: NewPlotTune jukeboxName|# tuneNumber [displayRows _ 8secondsPerRow]"; GOTO Quit; }; IF argv[1].Equal["#"] THEN { IF Jukebox.instances=NIL THEN { error _ "No jukebox open"; GOTO Quit}; jukebox _ Jukebox.instances.first; } ELSE jukebox _ Jukebox.FindJukebox[argv[1]]; IF jukebox = NIL THEN { jukebox _ Jukebox.OpenJukebox[argv[1]]; weOpened _ TRUE }; tuneID _ Convert.IntFromRope[argv[2]]; tune _ Jukebox.OpenTune[self: jukebox, tuneId: tuneID, write: FALSE]; tuneSize _ Jukebox.TuneSize[tune]; data.jukeboxName _ LastComponentWithoutExtension[jukebox.jukeboxName]; IF tuneSize = 0 THEN {error _ "Tune contains no chirps, so I won't plot them"; GOTO Quit}; data.rows _ IF argv.argc = 3 THEN MAX[(tuneSize+4)/8, 1] ELSE Convert.IntFromRope[argv[3]]; IF data.rows > maxRows THEN data.rows _ maxRows; IF data.rows = 0 THEN {error _ "It didn't take me long to plot no rows!"; GOTO Quit}; sampleIncrement _ tuneSize*Jukebox.bytesPerChirp/(data.rows*maxWidth); IF sampleIncrement > Jukebox.bytesPerChirp -- some tune: will have to add more rows !! THEN { data.rows _ (tuneSize + (maxWidth -1))/maxWidth; sampleIncrement _ Jukebox.bytesPerChirp; IF data.rows > maxRows THEN ERROR; data.width _ (tuneSize+(data.rows-1))/data.rows } ELSE { WHILE (Jukebox.bytesPerChirp MOD sampleIncrement) # 0 DO sampleIncrement _ sampleIncrement + 1 ENDLOOP; data.width _ ((Jukebox.bytesPerChirp/sampleIncrement)*tuneSize+(data.rows-1))/data.rows }; pointsPerChirp _ Jukebox.bytesPerChirp/sampleIncrement; data.totalHeight _ data.rows*(top+data.height+bottom); FOR i: INT IN [0..data.rows) DO data.points[i] _ NEW [ArrayOfPoints[data.width]] ENDLOOP; FOR chirp: INT IN [0..tuneSize) DO energyBlock: TuneAccess.EnergyBlock _ TuneAccess.GetEnergyProfile[jukebox, tune, chirp, , pointsPerChirp]; FOR element: INT IN [0..pointsPerChirp) DO { peakEnergy _ MAX [peakEnergy, energyBlock[element]]; currPlotPoint _ AddPoint[currPlotPoint: currPlotPoint, data: data, value: energyBlock[element]] } ENDLOOP ENDLOOP; IF peakEnergy = 0 THEN {error _ "No energy in this file at all!"; GOTO Quit}; { positionInChirp: INT _ 0; -- used to tell when to get a new ambient level hangoverInPoints: INT _ Jukebox.hangoverPackets*pointsPerChirp/ INT[(Jukebox.bytesPerChirp/Jukebox.singlePktLength)]; pointsSinceLastNonSilence: INT _ hangoverInPoints; ambientLevel: Jukebox.EnergyRange; currChirp: INT _ 0; FOR i: INT IN [0..data.rows) DO FOR j: INT IN [0..data.width) DO IF positionInChirp = 0 THEN { ambientLevel _ TuneAccess.ReadAmbientLevel[jukebox, tune, currChirp ! Jukebox.MissingChirp => {ambientLevel _ LAST[Jukebox.EnergyRange]; CONTINUE}; Jukebox.EOF => CONTINUE]; -- latter may occur if there are more points in the graph than pointsPerChirp*lengthInChirps due to integer division etc. currChirp _ currChirp + 1 }; pointsSinceLastNonSilence _ IF data.points[i][j] <= ambientLevel THEN pointsSinceLastNonSilence + 1 ELSE 0; IF pointsSinceLastNonSilence > hangoverInPoints THEN data.points[i][j] _ -data.points[i][j]; positionInChirp _ (positionInChirp+1) MOD pointsPerChirp ENDLOOP ENDLOOP }; Jukebox.ArchiveCloseTune[jukebox, tune]; IF weOpened THEN jukebox _ Jukebox.CloseJukebox[jukebox]; cmd.out.PutF["Energy peak in file is %d - graph is scaled accordingly\n", IO.int[peakEnergy]]; data.yMax _ peakEnergy; data.tuneString _ Rope.Cat[IF tuneID < 10 THEN "Tune " ELSE "Tune", Convert.RopeFromInt[tuneID]]; data.tuneNumberFont _ IF tuneID < 100 THEN data.helvetica10 ELSE data.helvetica8; RETURN[TRUE] }; EXITS Quit => { IF tune # NIL THEN Jukebox.ArchiveCloseTune[jukebox, tune]; IF weOpened AND jukebox # NIL THEN jukebox _ Jukebox.CloseJukebox[jukebox]; RETURN [FALSE, error] }; }}; PlotTune: Commander.CommandProc = { data: Data _ NEW [DataRecord]; parsedOkay: BOOLEAN; data.commandName _ plotTune; data.height _ 60; [parsedOkay, msg] _ ParseTuneProfile[data, cmd]; IF ~parsedOkay THEN RETURN [$Failure, msg]; data.container _ Containers.Create[[ name: "Tune Energy Time Profile", iconic: FALSE, icon: Icons.NewIconFromFile["JukeboxIcons.icon", 10], -- pro tem., until Polle produces a graph column: left, menu: NIL, scrollable: FALSE, data: data ]]; ViewerOps.SetOpenHeight[data.container, data.totalHeight]; data.graphs _ ViewerOps.CreateViewer[ flavor: $TunePlot, info: [ parent: data.container, wx: 0, wy: 0, ww: left+maxWidth+left, -- this means the window is still standard left-column width, even though the graph may be narrower: this is judged more visually pleasing wh: data.totalHeight, scrollable: FALSE, data: data] ]; [] _ ViewerEvents.RegisterEventProc[Poof, destroy, data.graphs, TRUE]; }; EnergyArray: TYPE = REF EnergyValueArray; EnergyValueArray: TYPE = ARRAY Jukebox.EnergyRange OF INT; ParseTuneEnergies: PROC [data: Data, cmd: Commander.Handle] RETURNS [succeeded: BOOLEAN _ FALSE, error: Rope.ROPE _ NIL] = TRUSTED { weOpened: BOOL _ FALSE; jukebox: Jukebox.Handle _ NIL; tune: Jukebox.Tune _ NIL; {{ ENABLE Jukebox.Error => { error _ rope; GOTO Quit }; argv: CommandTool.ArgumentVector; tuneSize: INT; tuneID: INT; currPlotPoint: INT _ 0; energyMax: Jukebox.EnergyRange _ 0; energyPeak: Jukebox.EnergyRange _ 0; levelsPerCoarsePixel: INT; energyProfile: EnergyArray _ NEW [EnergyValueArray _ ALL[0]]; fineLower, fineUpper: Jukebox.EnergyRange; argv _ CommandTool.Parse[cmd ! CommandTool.Failed => {error _ errorMsg; GOTO Quit}]; IF argv.argc # 3 THEN { error _ "Usage: NewPlotTune jukeboxName|# tuneNumber"; GOTO Quit }; IF argv[1].Equal["#"] THEN { IF Jukebox.instances=NIL THEN { error _ "No jukebox open"; GOTO Quit}; jukebox _ Jukebox.instances.first; } ELSE jukebox _ Jukebox.FindJukebox[argv[1]]; IF jukebox = NIL THEN { jukebox _ Jukebox.OpenJukebox[argv[1]]; weOpened _ TRUE }; tuneID _ Convert.IntFromRope[argv[2]]; tune _ Jukebox.OpenTune[self: jukebox, tuneId: tuneID, write: FALSE]; tuneSize _ Jukebox.TuneSize[tune]; data.jukeboxName _ LastComponentWithoutExtension[jukebox.jukeboxName]; IF tuneSize = 0 THEN {error _ "Tune contains no chirps, so I won't plot them"; GOTO Quit}; FOR chirp: INT IN [0..tuneSize) DO energyBlock: TuneAccess.EnergyBlock _ TuneAccess.GetEnergyProfile[jukebox, tune, chirp, , Jukebox.bytesPerChirp/Jukebox.singlePktLength]; FOR element: INT IN [0..Jukebox.bytesPerChirp/Jukebox.singlePktLength) DO energyProfile[energyBlock[element]] _ energyProfile[energyBlock[element]] + 1 ENDLOOP ENDLOOP; energyProfile[0] _ 0; FOR i: Jukebox.EnergyRange IN Jukebox.EnergyRange DO IF energyProfile[i] > energyProfile[energyPeak] THEN energyPeak _ i; IF energyProfile[i] > 0 THEN energyMax _ i ENDLOOP; levelsPerCoarsePixel _ ((energyMax+1) + (maxWidth-1))/maxWidth; data.width _ (energyMax+levelsPerCoarsePixel)/levelsPerCoarsePixel; data.rows _ 2; data.totalHeight _ data.rows*(top+data.height+bottom); FOR i: INT IN [0..data.rows) DO data.points[i] _ NEW [ArrayOfPoints[data.width]] ENDLOOP; { currEnergy: Jukebox.EnergyRange _ 0; FOR i: INT IN [0..data.width-2] DO accumulator: INT _ 0; FOR j: INT IN [0..levelsPerCoarsePixel) DO accumulator _ accumulator + INT[energyProfile[currEnergy]]; currEnergy _ currEnergy + 1 ENDLOOP; data.points[0][i] _ REAL[accumulator]/REAL[levelsPerCoarsePixel] ENDLOOP; { lastPoint: INT _ 0; DO lastPoint _ lastPoint + INT[energyProfile[currEnergy]]; IF currEnergy = energyMax THEN EXIT; currEnergy _ currEnergy + 1 ENDLOOP; data.points[0][data.width-1] _ REAL[lastPoint]/REAL[levelsPerCoarsePixel] }}; SELECT INT[energyPeak] FROM > LAST[Jukebox.EnergyRange] - (data.width+5)/6 => { fineUpper _ LAST[Jukebox.EnergyRange]; fineLower _ fineUpper - data.width/3 + 1 }; < FIRST[Jukebox.EnergyRange] + (data.width+5)/6 => { fineLower _ FIRST[Jukebox.EnergyRange]; fineUpper _ fineLower + data.width/3 - 1 }; ENDCASE => { fineLower _ energyPeak - data.width/6; fineUpper _ fineLower + data.width/3 - 1 }; FOR i: INT IN [0..data.width) DO data.points[1][i] _ energyProfile[fineLower+i/3] ENDLOOP; FOR i: INT IN [fineLower/levelsPerCoarsePixel..fineUpper/levelsPerCoarsePixel] DO IF i <= LAST[Jukebox.EnergyRange] THEN data.points[0][i] _ -data.points[0][i] ENDLOOP; Jukebox.ArchiveCloseTune[jukebox, tune]; IF weOpened THEN jukebox _ Jukebox.CloseJukebox[jukebox]; cmd.out.PutF["Highest energy value in file is %d: this is length of coarse plot x axis\nMost frequent energy level is %d occurences of %d:\n fine plot x axis runs from %d to %d (range shown gray on coarse plot)\n", IO.int[energyMax], IO.int[energyProfile[energyPeak]], IO.int[energyPeak], IO.int[fineLower], IO.int[fineUpper]]; data.yMax _ energyProfile[energyPeak]; data.tuneString _ Rope.Cat[IF tuneID < 10 THEN "Tune " ELSE "Tune", Convert.RopeFromInt[tuneID]]; data.tuneNumberFont _ IF tuneID < 100 THEN data.helvetica10 ELSE data.helvetica8; RETURN[TRUE] }; EXITS Quit => { IF tune # NIL THEN Jukebox.ArchiveCloseTune[jukebox, tune]; IF weOpened AND jukebox # NIL THEN jukebox _ Jukebox.CloseJukebox[jukebox]; RETURN [FALSE, error] }; }}; PlotDistribution: Commander.CommandProc = { data: Data _ NEW [DataRecord]; parsedOkay: BOOLEAN; data.commandName _ plotDistribution; data.height _ 150; [parsedOkay, msg] _ ParseTuneEnergies[data, cmd]; IF ~parsedOkay THEN RETURN [$Failure, msg]; data.container _ Containers.Create[[ name: "Tune Energy Distribution", iconic: FALSE, icon: Icons.NewIconFromFile["JukeboxIcons.icon", 15], -- pro tem., until Polle produces a graph icon column: left, menu: NIL, scrollable: FALSE, data: data ]]; ViewerOps.SetOpenHeight[data.container, data.totalHeight]; data.graphs _ ViewerOps.CreateViewer[ flavor: $TunePlot, info: [ parent: data.container, wx: 0, wy: 0, ww: left+maxWidth+left, -- this means the window is still standard left-column width, even though the graph may be narrower: this is judged more visually pleasing wh: data.totalHeight, scrollable: FALSE, data: data] ]; [] _ ViewerEvents.RegisterEventProc[Poof, destroy, data.graphs, TRUE]; }; graphClass: ViewerClasses.ViewerClass _ NEW[ViewerClasses.ViewerClassRec _ [paint: RepaintViewer]]; ViewerOps.RegisterViewerClass[$TunePlot, graphClass]; Commander.Register["PlotTune", PlotTune, "PlotTune jukeboxName|# tuneNumber [displayRows _ 8secondsPerRow] - plot the content of a tune in graphical form: # represents the first currently open jukebox"]; Commander.Register["PlotDistribution", PlotDistribution, "PlotDistribution jukeboxName|# tuneNumber - plot the distribution of energies in a jukebox tune: # represents the first currently open jukebox"]; }. ÜTuneGraphsImpl.mesa programs to plot the energy profile and distribution of a tune Ades, March 6, 1986 4:34:12 pm PST variables and routines used by both commands [self: ViewerClasses.Viewer, context: Imager.Context, whatChanged: REF ANY, clear: BOOL] RETURNS [quit: BOOL _ FALSE] routines used to implement PlotTune only because of the way that Tuneaccess.GetEnergyProfile works, we want to sample an integral number of times per chirp. The number of samples may not add up to a multiple of maxWidth, in which case set data.width to a lower value the above paragraph built up the points on the graph. We want now to shade gray the points which are silent according to the ambient level values. The Voicestreams software samples every 20ms to do this rather than at the intervals we did above, but we'll use the sampling rate above because otherwise we'll get some odd edging effects. We'll multiply all the data points by -1 if they are to be made gray, a convention that the PlotData routine understands we use ArchiveCloseTune so that the read/write dates will not be altered by this command routines used to implement PlotDistribution only the first task is to build up an array depicting the occurences of each energy level we are not interested in the (potentially large) number of zero energy occurences program plots out two lines of information: on the first is a histogram 0..energyMax of the energy occurences [averaged over the several levels represented by a single point on this coarse plot] and on the second a plot one level per pixel plot centred on the energyPeak, i.e. frequency maximum: the area of the second is shown gray on the first. We'll set the same y-height for both axes: on the gray region we may get against-the-rails behaviour but this doen't matter handle data.width-1 specially in case of bounds problems now the fine plot and shade the fine plot regions gray on the coarse plot we use ArchiveCloseTune so that the read/write dates will not be altered by this command ʘšœS™SIcode™"J˜—šÏk ˜ Jšœ œ!˜0Jšœ œ!˜2Jšœ œ˜%Jšœœ˜)Jšœœ˜Jšœœœ˜$Jšœœ[˜gJšœœ˜&Jšœ œ˜)JšœœÁ˜ÎJšœœœ ˜5Jšœœœ˜Jšœ œ ˜2Jšœœ2˜EJšœ œ4˜CJšœ œ3˜CJ˜—šœœ˜š˜Jšœ4œM˜ƒJ˜—Jšœ œ˜Jšœœ œ˜Jšœœ˜Jšœ œ ˜1J˜J™,J˜Jšœœœ ˜šœ œœ˜Jšœ"œ˜&Jšœœ˜#J˜J˜Jšœœœ˜Jšœœœ˜Jšœœ˜ Jšœœ˜Jšœœ˜ Jšœœ˜ Jšœ œ˜Jš œœœœœ˜CJ˜JšœB˜BJšœB˜BJšœD˜DJ˜J˜—Jšœ œ˜Jšœ œ˜Jšœœ˜Jšœœ˜Jšœœ˜Jšœœ ˜+Jšœœ˜Jšœ œ˜Jšœ<˜œ˜EJšœ"˜"J˜JšœF˜FJ˜Jšœœ;œ˜ZJš œ œœœœ˜[Jšœœ˜0Jšœœ5œ˜UJ˜Jšœá™áJšœF˜FJšœ)ž+˜VJšœ˜šœ3˜3Jšœ(˜(Jšœœœ˜"Jšœ/˜/—J˜Jšœ˜šœ˜Jšœœœ'œ˜gJšœW˜W—J˜J˜Jšœ7˜7J˜Jšœ6˜6šœœœ˜Jšœœ˜0—Jšœ˜J˜šœœœ˜"Jšœj˜jJšœ œœ˜*šœœ$˜7Jšœ_˜_—J˜Jš˜—Jšœ˜J˜Jšœœ,œ˜MJ˜J™Éšœœž/˜LJšœœ+œ2˜uJšœœ˜2Jšœ"˜"Jšœ œ˜J˜šœœœ˜šœœœ˜ Jšœœ˜šœ˜Jš œnœœ œœžy˜§J˜—J˜Jšœœ#œ œ˜lJšœ.œ(˜\Jšœ&œ˜8—Jš˜—Jš˜—˜J˜—J˜(JšœX™XJšœ œ)˜9JšœJœ˜^J˜Jšœ˜Jšœa˜aJšœR˜RJšœœ˜ J˜Jš˜šœ ˜ Jšœœœ)˜;Jšœ œ œœ)˜KJšœœ˜Jšœ˜—J˜—J˜š¡œ˜#Jšœ œ˜Jšœ œ˜J˜J˜J˜Jšœ0˜0Jšœ œœ˜+J˜šœ$˜$Jšœ!˜!Jšœœ˜Jšœ6ž)˜_Jšœ ˜ Jšœœ˜ Jšœ œ˜Jšœ˜—Jšœ:˜:šœ%˜%Jšœ˜šœ˜Jšœ˜Jšœ˜Jšœ˜JšœžŠ˜¢Jšœ˜Jšœ œ˜Jšœ˜——Jšœ@œ˜FJšœ˜—J˜J™0J™Jšœ œœ˜)š œœœœœ˜:J˜—šŸœœ%œ œœœœœ˜„Jšœ œœ˜Jšœœ˜Jšœœ˜J˜šœ˜Jšœœ˜J˜—Jšœ!˜!Jšœ œ˜Jšœœ˜ Jšœœ˜J˜#J˜$Jšœœ˜Jšœœœ˜=J˜*J˜JšœHœ˜Tšœœ˜Jšœ6˜6Jšœ˜ J˜—šœœ˜Jšœœœœ˜FJšœ"˜"J˜—Jšœ(˜,šœ œœ˜J˜'Jšœ ˜Jšœ˜—Jšœ&˜&Jšœ>œ˜EJšœ"˜"J˜JšœF˜FJ˜Jšœœ;œ˜ZJ˜J™Tšœœœ˜"Jšœ‰˜‰šœ œœ4˜IJšœN˜N—Jš˜—Jšœ˜ J™QJšœ˜—˜šœœ˜4Jšœ.œ˜DJšœœ˜*—Jšœ˜J˜Jšœ×™×J˜Jšœ?˜?JšœC˜CJšœ˜Jšœ6˜6šœœœ˜Jšœœ˜0—Jšœ˜J˜˜'Jšœœœœ˜#šœ8™8Jšœ œ˜šœœœ˜*Jšœœ˜;J˜—Jšœ˜Jšœœœ˜@—Jšœ˜—˜Jšœ œ˜Jš œœœœœœ˜…Jšœœ œ˜I—Jšœ˜J˜J™Jšœœ ˜šœœ+˜1šœœ˜*Jšœ+˜+——šœœ+˜2šœœ˜+Jšœ+˜+——šœ˜ šœ*˜*šœ+˜+J˜———šœœœ˜ Jšœ0˜0—Jšœ˜J˜J™7šœœœB˜QJšœœœ'˜N—Jšœ˜J˜J˜(JšœX™XJšœ œ)˜9Jš œ×œœ!œœœ˜ÇJ˜Jšœ&˜&Jšœa˜aJšœQ˜QJšœœ˜ J˜Jš˜šœ ˜ Jšœœœ)˜;Jšœ œ œœ)˜KJšœœ˜Jšœ˜—J˜—J˜š¡œ˜+Jšœ œ˜Jšœ œ˜J˜$J˜J˜Jšœ1˜1Jšœ œœ˜+J˜šœ$˜$Jšœ!˜!Jšœœ˜Jšœ6ž.˜dJšœ ˜ Jšœœ˜ Jšœ œ˜Jšœ˜—Jšœ:˜:šœ%˜%Jšœ˜šœ˜Jšœ˜Jšœ˜Jšœ˜JšœžŠ˜¢Jšœ˜Jšœ œ˜Jšœ˜——Jšœ@œ˜FJšœ˜J˜—šœ(œ˜JJšœ˜—Jšœ5˜5JšœË˜ËJšœË˜ËJ˜Jšœ˜——…—DÒ^Ç