<> <> 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 = { <<[self: ViewerClasses.Viewer, context: Imager.Context, whatChanged: REF ANY, clear: BOOL] RETURNS [quit: BOOL _ FALSE]>> 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"]; }.