Thursday, October 27, 2005

objects needing more comments

ctrlresize, replacefilesdlg, masterdlg, parmrow, playlist, numbersdlg, viewdialog

Tuesday, October 25, 2005

adding HLS color to existing snapshots


ar << sizeof(RING);
ar << m_Ring.GetCount();
POSITION pos = m_Ring.GetHeadPosition();
for (int i = 0; i < m_Ring.GetCount(); i++) {
RING rp = m_Ring.GetNext(pos);
double h, l, s;
int c = rp.Color;
CHLS::rgb2hls(GetRValue(c) / 255.0,
GetGValue(c) / 255.0, GetBValue(c) / 255.0, h, l, s);
rp.Hue = h;
rp.Lightness = l;
rp.Saturation = s;
ar.Write(&rp, sizeof(RING));
}

Monday, October 24, 2005

Movie format could save space

The movies are using the snapshot format, which includes all of the ring info. In the ring info, the rotation and shift deltas and the HLS color are not used at all during movie playback. These attributes constitute 40 bytes out of 120, or one third of the total size of the ring structure. The actual savings could be nearly a third, since most of the space in a movie is taken up by rings (hundreds of rings per frame).

The savings would require adding a second serialization method, e.g. MiniSerialize, which would be used by the SnapMovie object. The method could be optimized by placing the deltas and the HLS color at the start of the ring structure, because then it would still be possible to do a single write directly from the ring, instead of copying to an intermediate structure first.

A problem with this approach is that it would longer be possible to export a complete (non-mini) snapshot from a movie, since some of the information would be missing. The HLS color could be restored from RGB, but not the deltas. On the other hand the deltas aren't useful in snapshots so maybe it wouldn't matter.

NOTE that WinZip reduces Steve1.whm by 61%, and Steve2.whm by 73%.

Space-saving benchmarks:

60 seconds, 25 FPS, default speed, window maximized but not full-screen, wait for full ring count before starting to record, sizes in MB, ring counts are rough averages

patch rings old new less
kaleid 1000 172.5 108.8 37%
pinwheel 500 98.0 62.5 36%
lotus light 300 71.4 46.5 35%
cross 150 34.6 26.3 24%

useful nugget of ring conversion code:

nr.RotDelta = rp.RotDelta;
nr.ShiftDelta = rp.ShiftDelta;
nr.Hue = rp.Hue;
nr.Lightness = rp.Lightness;
nr.Saturation = rp.Saturation;
nr.Rot = rp.Rot;
nr.Steps = rp.Steps;
nr.Scale = rp.Scale;
nr.Shift = rp.Shift;
nr.StarRatio = rp.StarRatio;
nr.Sides = rp.Sides;
nr.Delete = rp.Delete;
nr.Reverse = rp.Reverse;
nr.Color = rp.Color;
nr.Pinwheel = rp.Pinwheel;
nr.LineWidth = rp.LineWidth;
nr.DrawMode = rp.DrawMode;
nr.Spacing = rp.Spacing;

Thursday, October 20, 2005

array of CArrays not optimized correctly

It looks like dereferencing an array of CArray objects can causes MFC to call the [] operator, instead of inlining it, even in Release mode. Presumably this is why:

int CPlaylistDlg::FindHotKey(DWORD HotKey) const
{
int Patches = GetCount();
for (int i = 0; i < Patches; i++) {
if (HotKey == m_Bank[m_CurBank][i].m_HotKey)
return(i);
}
return(-1);
}

generates twice as much code as:

int CPlaylistDlg::FindHotKey(DWORD HotKey) const
{
int Patches = GetCount();
for (int i = 0; i < Patches; i++) {
if (HotKey == (*m_Patch)[i].m_HotKey)
return(i);
}
return(-1);
}

Oddly, this is bad too:

int CPlaylistDlg::FindHotKey(DWORD HotKey) const
{
int Patches = GetCount();
const PATCH_LIST *p = &m_Bank[m_CurBank];
for (int i = 0; i < Patches; i++) {
if (HotKey == (*p)[i].m_HotKey)
return(i);
}
return(-1);
}

but this is fine (1 line longer than original):

int CPlaylistDlg::FindHotKey(DWORD HotKey) const
{
int Patches = GetCount();
for (int i = 0; i < Patches; i++) {
if (HotKey == m_Bank[m_CurBank].GetData()[i].m_HotKey)
return(i);
}
return(-1);
}

Saturday, October 08, 2005

MainFrame bloat

2662 lines and counting, ouch
non-message handlers 1139
message map 166
message handlers 1200

things that could move easily:
ShowDemo: only 32 lines but could get bigger

not so easy
GetInput: 83 lines and could get bigger

odd shift


double xshift[2] = {rp.Shift.x * m_st.Zoom, (rp.Shift.x + rp.OddShift.x) * m_st.Zoom};
double yshift[2] = {rp.Shift.y * m_st.Zoom, (rp.Shift.y + rp.OddShift.y) * m_st.Zoom};

Notice that the calculation is NOT corrected for radius. This causes the effect to decrease with distance from the origin. Correcting for radius gives a very different and less pleasing effect. The display becomes too busy, and at extreme values, the image is distorted into a narrow cylinder. The first objection could possibly be addressed by limiting the size of the ring list.

To correct the above for radius:
double xshift[2] = {rp.Shift.x * m_st.Zoom, (rp.Shift.x + rp.OddShift.x * rp.Steps) * m_st.Zoom};
double yshift[2] = {rp.Shift.y * m_st.Zoom, (rp.Shift.y + rp.OddShift.y * rp.Steps) * m_st.Zoom};

Some possible names for "odd shift":

Shear: incorrect usage
Extrude: incorrect usage
Tilt: too vague
Tunnel: too vague, also other parameters can produce this effect
Relief: too vague
Stamen: too obscure
Center Fold: misleading, the new version corrects for radius, see below
Splay: means legs spread at various angles, not various lengths
Deform: vague, not a noun, misleading negative connotations
Bias: misleading connotation of unfairness
Fold: vague, possibly misleading
Asymmetry: vague, too long a word

10/29/05 corrected for radius but also centered, much better

DPOINT splay = {rp.Splay.x * rp.Steps, rp.Splay.y * rp.Steps};
double xshift[2] = {
(rp.Shift.x - splay.x) * m_st.Zoom,
(rp.Shift.x + splay.x) * m_st.Zoom};
double yshift[2] = {
(rp.Shift.y - splay.y) * m_st.Zoom,
(rp.Shift.y + splay.y) * m_st.Zoom};

Controls should be polar as with Skew: Splay Radius, Splay Angle

Some interesting interdependent terms:
Contort: Twist, wrench, or bend severely out of shape
Pinch: To press, squeeze, or bind painfully
Buckle: Bending, warping, or crumpling; a bend or bulge
Rumple: An irregular or untidy crease
Crumple: To crush together or press into wrinkles; rumple
Crease: A line made by pressing, folding, or wrinkling
Wrinkle: A small furrow, ridge, or crease on a normally smooth surface
Pucker: To gather into small wrinkles or folds

And the winner as of 12/17/05 is:
PUCKER

totally cool demo: default patch, ring spacing 11.192, pucker radius -.406, pucker angle -65.88 / ramp up / 180 / .08, master speed 222

another one: patch_051028050234, rings = 75, pucker rad = .268, angle = ramp up / 180 / .5, speed = 495

optimized 12/19/05 (two less Zoom multiplications):

DPOINT pucker = {rp.Pucker.x * steps, rp.Pucker.y * steps};
DPOINT shift = {rp.Shift.x * m_st.Zoom, rp.Shift.y * m_st.Zoom};
double xshift[2] = {shift.x - pucker.x, shift.x + pucker.x};
double yshift[2] = {shift.y - pucker.y, shift.y + pucker.y};

ring list size limit


in AddRing, at the very end:
while (m_Ring.GetCount() > whatever) {
if (Ring.Reverse)
m_Ring.RemoveHead();
else
m_Ring.RemoveTail();
}

Bézier curves


in Draw:
#if 1
m_pa[i] = m_pa[0]; // close the shape
Polyline(dc, m_pa, sides + 1);
#else
CPoint pt2[200];
int j = 0;
for (int i = 0; i < sides; i++) {
pt2[j++] = m_pa[i];
if (i & 1)
pt2[j++] = CPoint(m_pa[i].x, m_pa[i].y);
}
pt2[j++] = pt2[0];
PolyBezier(dc, pt2, j);
#endif

Saturday, October 01, 2005

scene rotation


in DefState:
0 // Rotation

in Addring:
Ring.Rot = Ring.RotDelta * Offset + m_st.Rotation;

void CWhorldView::Rotate(double Degrees, bool Repaint)
{
POSITION pos = m_Ring.GetHeadPosition();
double r = DTR(Degrees);
while (pos != NULL) {
RING& Ring = m_Ring.GetNext(pos);
Ring.Rot += r;
}
m_st.Rotation = fmod(m_st.Rotation + r, 2 * PI); // wrap to limit magnitude
if (Repaint)
Invalidate();
}

Note that this causes unexpected behavior (addition, subtraction, cancelation) when rotation is inverse of LFO rotation. Also X/Y shifts are NOT rotated, so for example if Aspect Ratio is 2, the asymmetry will remain orthogonal instead of rotating. Solving this would require changes to the drawing code.

benchmarks for movie recording

------------
PIII, Kaleid, 1024 x 768, 30 FPS, speed = 680, 2500 samps: min = .001197, max = .004851, avg = .001821, sdev = .000362

avg .002 = .06 @ second = 6% avg usage for recording
-----------
AMD, Kaleid, 1024 x 768, 30 FPS, speed = 680, 2500 samps: min = .000167, max = .000611, avg = .000322, sdev = 8.8329E-05

avg .0003 = .009 @ second = .9% avg usage for recording
------------
AMD test repeated over 5 minutes: min = .000172, max = .005491, avg = .000485, sdev = .000129

all samples were < than 1 ms except for a single single spike of 0.005491 at 3:38
------------

glitch #1: Jeff & Sean

Sean:

1. Visible tearing of image: DirectX is supposed to care of this no? Would full-screen mode affect it? Possibly affected by frame rate and CPU loading.

2. Should sync to beat automatically.

3. Should support multiple trackballs and other input devices at once.

Jeff:

1. Support for MIDI notes would be helpful since he's using an Oxygen 8. Notes could act as toggles for the function keys.

2. Tempo nudge should NOT resync, or at least there should be an alternate nudge that doesn't resync. Sean agrees.

3. Rotation of the entire image (as opposed to rotate speed), especially via MIDI.

4. Zoom should go further out: can do this with MIDI.

Wednesday, September 28, 2005

movie disk requirements

The data rate of a WHM movie file is linearly proportional to scene complexity (as measured by ring count), and is also affected by the frame rate. Typical data rates range from .4 - 3.0 MBytes @ second, which means a 1-hour recording can require anywhere from 1.5 to 10 GB.

Both ATA 100 and Firewire A can sustain write data rates of at least 30 MB @ second, with minimal CPU loading, so performance probably isn't an issue. Disk space could be an issue, particularly on older machines.

Using WinZip on a WHM file typically yields a 60% - 70% size reduction. Recording to a compressed folder yields a 40% - 60% reduction, but causes the display to freeze at regular intervals on a PIII machine. The Gnu zlib compression library might have less impact, but some increase in CPU load is inevitable. To reduce disk usage, it might make sense to compress WHM files after recording. The app could include an option that does this automatically, but it would have to be done from a separate low-priority thread, and even then performance might be affected by context switching.

A higher-order solution would record user input instead of recording ring data. This would reduce the data rate to a negligible amount, but at the expense of greatly increased complexity and fragility. This method should be explored for a future version.

A very rough estimate of the data rate resulting from input recording:

Record cursor delta for every frame: 8 bytes (2 ints) @ frame
Record 16 MIDI controllers for every frame (worst case): 48 bytes @ frame
Keyboard commands are too infrequent to affect the estimate
8 + 48 = 56 bytes per frame; at 30 FPS, 56 * 30 = 1680 bytes @ second
That's 6MB per hour in the absolute worst case

Some benchmarks:

Uncompressed 1024 x 768 video at 30 FPS: 70.5 MB @ sec
Lotus light, speed around 700, 30 FPS, 1 minute = 131 MB (2.2 MB @ sec)
Cross, speed around 700, 30 FPS, 1:36 = 80 MB (.83 MB @ sec)
Kaleid, speed around 700, 30 FPS, 1:36 = 282 MB (2.9 MB @ sec)

Another interesting possibility: Recording the view's oscillators and STATE member but not the rings, and then regenerating the rings on playback/export

1 oscillator = 44 bytes * 16 = 704 bytes @ frame
view STATE member = 104 bytes + 704 = 808 bytes @ frame
call it 1K bytes @ frame, at 30 FPS = 30K bytes @ second (108 MB @ hour)

Thursday, September 22, 2005

making a WMV-9 DVD

Toshi says no need to change frame rate for WMV; between 5 and 20 MBits per sec should be good, and 1024 x 768 is fine. Must compress the bitmaps first to make enough room to create an AVI, expect on the order of 75% compression overall.

Monday, September 12, 2005

additional MIDI control

on/off: mirror, fill, outline, xray, invert hue, invert fill, invert outline, invert xray, rotate hue, reverse

zoom should be damped, no?

Saturday, September 10, 2005

show size in status bar message while resizing

You must handle WM_ENTERSIZEMOVE and wM_EXITSIZEMOVE to show the initial size and restore the default message.

LRESULT CMainFrame::OnEnterSizeMove(WPARAM wParam, LPARAM lParam)
{
CRect r;
m_View->GetClientRect(r);
CString s;
s.Format("%d x %d\n", r.Width(), r.Height());
SetMessageText(s); // must do all the above in OnSize too, but only if window is visible
return(TRUE);
}

LRESULT CMainFrame::OnExitSizeMove(WPARAM wParam, LPARAM lParam)
{
SetMessageText(AFX_IDS_IDLEMESSAGE);
return(TRUE);
}

Note that this only works if the system property "show window contents while dragging" is set. Otherwise you only see the initial size, because WM_SIZE isn't sent until the drag ends. WM_SIZING *is* sent, but it doesn't help, because it passes us the size of the frame, whereas we want the size of the view. Oddly, there seems to be no way to determine the client area that would result from a given window size. CalcWindowRect does the reverse: it gives us the size of the window needed for a given client area.

Tuesday, September 06, 2005

CalcWindowRect bug

This bug first appeared during the development of movie export. Moving the export to a separate process made it go away, but now it's back in version 1.0.73. Most likely the AVI file DLL was falsely accused and the movie export could have been part of the main app, but that's a secondary issue.

The symptom is, the app crashes in RowDialogForm's CalcWindowRect during the construction of the Parms dialog. It doesn't crash in debug, only in release, and creating a console window makes it go away (great!). Deleting the unused m_Template member variable from CPersistDlg.h also makes it go away. This last symptom clearly points towards a memory corruption problem of some kind. In the release debugger, the problem begins when CRowDialog::OnInitDialog calls CViewDialog::OnInitDialog. After this call, CRowDialog's "this" and HWND are trashed. Note that neither CViewDialog nor its immediate base class (CPersistDlg) override OnInitDialog, so this is effectively a call to CDialog::OnInitDialog.

Removing the call to MakeVJAccelTable in CMainFrame's ctor also makes it go away.

The really bad news: the app only crashes when you execute it from the IDE; running from Explorer is OK.

Other observations:
in Frame's ctor list, m_ResultsDlg(m_View), is incorrect initialization, m_View isn't the dialog's parent and in any case it's NULL! deleting this line doesn't help though...

could be passing 'this' to dialogs isn't such a good idea after all, seems like CMainFrame's 'this' keeps changing somehow or is that just the debugger being bad in release mode?

The solution: in CalcWindowRect, casting GetParent() down to CRowDialog isn't always correct. Initially, the parent is the dummy frame, not CRowDialog. Use a dynamic downcast to determine the parent.

The moral of the story: Downcasting is dangerous so use dynamic downcast and verify the result!

Monday, August 29, 2005

movies

pjbmp2avi.exe seems to work fine, with the CinePack codec at 100% (default) it's about 11MB for 36 seconds.

Storing snapshots with window size at 720x480 (DVD fullscreen) yields 80K snaps on average, with no appreciable slowdown: 80 * 25 * 60 = 120MB @ minute. This seems better than storing bitmaps.

Actual date rate test: demo speed = 975, 18.4 minutes, size 1.256 GB, rate = 73MB @ minute

Random waveform bug can be demonstrated using new OscTest, settings: random, .01, .5, .05, .1

Sunday, August 28, 2005

almost there

During crossfade, random waveform's frequency change glitches horribly, fix this. Also fix a minor glitch when patch is switched without crossfade: in this case clear the view's previous parameter info to prevent interpolation. Ring growth and master speed glitch too but it's less noticeable.

Overall it looks gorgeous and feels incredibly powerful and exciting, it's a different whorld. Master speed of 901 is very nice, maybe use this speed for demo? Maybe a bit too fast, 603 is good too...

Random waveform bug can be demonstrated using new OscTest, settings: random, .01, .5, .05, .1

Saturday, August 27, 2005

more on clumping

oscillator must move into view so it can be subclocked (non-integer clock increments). See 1.0.69c.

Wednesday, August 24, 2005

clumping

The clumping problem goes all the way back to the beginning. It occurs when master speed and/or ring growth) are big enough to cause multiple rings per timer tick. When the view's timer hook adds multiple rings, the rings all share the same properties, which causes those rings to form an easily distinguishable group or "clump".

The worst offenders are rotation and skew (AKA shift). Both of these are zeroed at birth, and then incremented by the view's draw function. A major reduction of clumping can be obtained simply by offsetting each ring's initial rotation and skew. The offset is a delta multiplied by a fraction. The fraction is calculated by dividing the ring's index by the total number of rings that will be born for this timer tick. If four rings will be born, the fractions for the rings will be 0, .25, .5, and .75 respectively, and if the current rotation delta is .2, the initial rotations for the rings will be 0 * .2, .25 * .2, .5 * .2, and .75 * .2.

The only complication is that the rings are generated in reverse order. This means the indices have to be inverted, e.g. by using (count - 1 - i) instead of i, which means the count must be computed before the ring-adding loop begins. The count can be computed as:

int(m_st.RingOffset / m_Parms.RingSpacing) + 1;

Also note that the resulting offsets must be multiplied by RingGrowth to be effective.

The next-worst offenders are the LFOs. The LFOs are outside the view, but they only modify the view parameters on timer tick boundaries. This means that any LFO has the potential to cause clumping. The high road would move the oscillators into the view but this is a drastic change and has many side effects. The low road is to interpolate between the current and previous parameter values. In practice interpolation seems to work very well for all parameters except ring growth and ring sides. The view timer hook must calculate a delta between each parameter's current and previous values, like so:

for (int j = 0; j < 16; j++)
delta[j] = (((double *)&prevparm)[j] - ((double *)&m_Parms)[j]) / count;

And then within the ring-adding loop, overwrite m_Parms with interpolated values:

for (j = 0; j < 16; j++) // NOTE: don't do this to ring growth or # sides
((double *)&m_Parms)[j] = ((double *)&prevparm)[j] + delta[j] * (count - 1 - i);

And finally, restore m_Parms.

Interestingly, this seems to work perfectly with sine LFOs, but exhibits a noticeable periodic discontinuity with triangle LFOs. This seems to indicate an undiscovered problem with the above method, or possibly a bug in the oscillator object.

The final offender is the ring color. This is calculated in the view's timer hook, which means all the rings in a group will have the exact same color. This can probably be solved by interpolation as well.