• MacTech Network:
  • Tech Support
  • |
  • MacForge.net
  • |
  • Apple News
  • |
  • Register Domains
  • |
  • SSL Certificates
  • |
  • iPod Deals
  • |
  • Mac Deals
  • |
  • Mac Book Shelf

MAC TECH

  • Home
  • Magazine
    • About MacTech in Print
    • Issue Table of Contents
    • Subscribe
    • Risk Free Sample
    • Back Issues
    • MacTech DVD
  • Archives
    • MacTech Print Archives
    • MacMod
    • MacTutor
    • FrameWorks
    • develop
  • Forums
  • News
    • MacTech News
    • MacTech Blog
    • MacTech Reviews and KoolTools
    • Whitepapers, Screencasts, Videos and Books
    • News Scanner
    • Rumors Scanner
    • Documentation Scanner
    • Submit News or PR
    • MacTech News List
  • Store
  • Apple Expo
    • by Category
    • by Company
    • by Product
  • Job Board
  • Editorial
    • Submit News or PR
    • Writer's Kit
    • Editorial Staff
    • Editorial Calendar
  • Advertising
    • Benefits of MacTech
    • Mechanicals and Submission
    • Dates and Deadlines
    • Submit Apple Expo Entry
  • User
    • Register for Ongoing Raffles
    • Register new user
    • Edit User Settings
    • Logout
  • Contact
    • Customer Service
    • Webmaster Feedback
    • Submit News or PR
    • Suggest an article
  • Connect Tools
    • MacTech Live Podcast
    • RSS Feeds
    • Twitter

Demonstration Program

Go to Contents
// 
// PreQuickDraw.c
// 
// 
// This program opens a window in which is displayed some information extracted from
// the GDevice structure for the main video device and some colour information extracted
// from the window's colour graphics port structure.  When the monitor is set to 256 
// colours or less, the colours in the colour table in the GDevice structure's pixel map
// structure are also displayed.
//
// A Demonstration menu, which is enabled if the monitor is a direct device set to 256
// colours or less at program start, allows the user to set the monitor to 16-bit colour,
// and restore the original pixel depth, using application-defined functions. 
//
// The program utilises 'MBAR', 'MENU', 'WIND', and 'STR#' resources, and a 'SIZE' 
// resource with the is32BitCompatible flag set.
//
// 

// ............................................................................. includes

#include <Appearance.h>
#include <Devices.h>
#include <Palettes.h>
#include <LowMem.h>
#include <Sound.h>
#include <ToolUtils.h>

// .............................................................................. defines

#define rMenubar              128
#define rWindow               128
#define mApple                128
#define  iAbout               1
#define mFile                 129
#define  iQuit                11
#define mDemonstration        131
#define  iSetDepth            1
#define  iRestoreDepth        2
#define rIndexedStrings       128
#define  sMonitorInadequate   1
#define  sSettingPixelDepth16 2
#define  sMonitorIsDepth16    3  
#define  sMonitorIsDepthStart 4 
#define  sRestoringMonitor    5
#define MAXLONG               0x7FFFFFFF
#define topLeft(r)            (((Point *) &(r))[0])
#define botRight(r)           (((Point *) &(r))[1])

// ..................................................................... global variables

Boolean  gDone;
SInt16   gStartupPixelDepth;

// .................................................................. function prototypes

void    main                        (void);
void    doInitManagers              (void);
void    doEvents                    (EventRecord *);
void    doDisplayInformation        (WindowPtr);
Boolean doCheckMonitor             (void);
void    doSetMonitorPixelDepth      (void);
void    doRestoreMonitorPixelDepth  (void);
void    doMonitorAlert              (Str255);

//  main

void  main(void)
{
  Handle        menubarHdl;
  MenuHandle    menuHdl;
  WindowPtr      windowPtr;
  Str255        theString;
  EventRecord    EventStructure;

  // ................................................................ initialise managers

  doInitManagers();
  
  // .......................................................... set up menu bar and menus
  
  menubarHdl = GetNewMBar(rMenubar);
  if(menubarHdl == NULL)
    ExitToShell();
  SetMenuBar(menubarHdl);

  menuHdl = GetMenuHandle(mApple);
  if(menuHdl == NULL)
    ExitToShell();
  else
    AppendResMenu(menuHdl,'DRVR');

  if(!(doCheckMonitor()))
  {
    GetIndString(theString,rIndexedStrings,sMonitorInadequate);
    doMonitorAlert(theString);
    menuHdl = GetMenuHandle(mDemonstration);
    DisableItem(menuHdl,0);
  }
  else
  {
    if(gStartupPixelDepth > 8)
    {
      menuHdl = GetMenuHandle(mDemonstration);
      DisableItem(menuHdl,0);
    }
  }
        
  DrawMenuBar();
  
  // ............................ open windows, set font size, show windows, move windows

  if(!(windowPtr = GetNewCWindow(rWindow,NULL,(WindowPtr)-1)))
    ExitToShell();

  SetPort(windowPtr);
  TextSize(10);


  // .................................................................... enter eventLoop

  gDone = false;

  while(!gDone)
  {
    if(WaitNextEvent(everyEvent,&EventStructure,MAXLONG,NULL))
      doEvents(&EventStructure);
  }
}

//  doInitManagers

void  doInitManagers(void)
{
  MaxApplZone();
  MoreMasters();

  InitGraf(&qd.thePort);
  InitFonts();
  InitWindows();
  InitMenus();
  TEInit();
  InitDialogs(NULL);

  InitCursor();  
  FlushEvents(everyEvent,0);

  RegisterAppearanceClient();
}

//  doEvents

void  doEvents(EventRecord *eventStrucPtr)
{
  SInt8     charCode;
  SInt32    menuChoice;
  SInt16    menuID, menuItem;
  SInt16    partCode;
  WindowPtr windowPtr;
  Str255    itemName;
  SInt16    daDriverRefNum;
  Rect      theRect;
  
  switch(eventStrucPtr->what)
  {
    case keyDown:
    case autoKey:
      charCode = eventStrucPtr->message & charCodeMask;
      if((eventStrucPtr->modifiers & cmdKey) != 0)
      {
        menuChoice = MenuEvent(eventStrucPtr);
        menuID = HiWord(menuChoice);
        menuItem = LoWord(menuChoice);
        if(menuID == mFile && menuItem  == iQuit)
          gDone = true;
      }
      break;
  
    case mouseDown:
      if(partCode = FindWindow(eventStrucPtr->where,&windowPtr))
      {
        switch(partCode)
        {
          case inMenuBar:
            menuChoice = MenuSelect(eventStrucPtr->where);
            menuID = HiWord(menuChoice);
            menuItem = LoWord(menuChoice);

            if(menuID == 0)
              return;

            switch(menuID)
            {
              case mApple:
                if(menuItem == iAbout)
                  SysBeep(10);
                else
                {
                  GetMenuItemText(GetMenuHandle(mApple),menuItem,itemName);
                  daDriverRefNum = OpenDeskAcc(itemName);
                }
                break;

              case mFile:
                if(menuItem == iQuit)
                  gDone = true;
                break;
  
              case mDemonstration:
                if(menuItem == iSetDepth)
                  doSetMonitorPixelDepth();
                else if(menuItem == iRestoreDepth)
                  doRestoreMonitorPixelDepth();
                break;
            }
            HiliteMenu(0);
            break;
          
          case inDrag:
            DragWindow(windowPtr,eventStrucPtr->where,&qd.screenBits.bounds);
            theRect = windowPtr->portRect;
            theRect.right = windowPtr->portRect.left + 250;
            InvalRect(&theRect);
            break;
        }
      }
      break;
      
    case updateEvt:
      windowPtr = (WindowPtr) eventStrucPtr->message;
      BeginUpdate(windowPtr);
      SetPort(windowPtr);
      EraseRect(&windowPtr->portRect);
      doDisplayInformation(windowPtr);
      EndUpdate(windowPtr);
      break;
  }
}

//  doDisplayInformation

void  doDisplayInformation(WindowPtr windowPtr)
{
  RGBColor      whiteColour = { 0xFFFF, 0xFFFF, 0xFFFF };
  RGBColor      blueColour  = { 0x4444, 0x4444, 0x9999 };
  GDHandle      deviceHdl;
  SInt16        videoDeviceCount = 0;  
  Str255        theString;
  SInt16        deviceType, pixelDepth, bytesPerRow;
  Rect          theRect;
  PixMapHandle  pixMapHdl;
  CGrafPtr      cgrafPtr;
  SInt32        pixelValue;
  SInt16        redComponent, greenComponent, blueComponent;
  CTabHandle    colorTableHdl;
  SInt16        entries = 0, a, b, c = 0;
  RGBColor      theColour;

  RGBForeColor(&whiteColour);
  RGBBackColor(&blueColour);
  EraseRect(&windowPtr->portRect);

  // .................................................................... Get Device List

  deviceHdl = LMGetDeviceList();

  // ................................................. count video devices in device list

  while(deviceHdl != NULL)
  {
    if(TestDeviceAttribute(deviceHdl,screenDevice))
      videoDeviceCount ++;

    deviceHdl = GetNextDevice(deviceHdl);
  }

  NumToString((SInt32) videoDeviceCount,theString);
  MoveTo(10,20);
  DrawString(theString);
  if(videoDeviceCount < 2)
    DrawString("\p video device in the device list.");
  else
    DrawString("\p video devices in the device list.");

  // .................................................................... Get Main Device

  deviceHdl = LMGetMainDevice();

  // .............................................................. determine device type

  MoveTo(10,35);
  if(BitTst(&(**deviceHdl).gdFlags,15 - gdDevType))
    DrawString("\pThe main video device is a colour device.");
  else
    DrawString("\pThe main video device is a monochrome device.");

  MoveTo(10,50);
  deviceType = (**deviceHdl).gdType;
  switch(deviceType)
  {
    case clutType:
      DrawString("\pIt is an indexed device with variable CLUT.");
      break;

    case fixedType:
      DrawString("\pIt is is an indexed device with fixed CLUT.");
      break;

    case directType:
      DrawString("\pIt is a direct device.");
      break;
  }

  // ............................................................ Get Handle to Pixel Map

  pixMapHdl = (**deviceHdl).gdPMap;

  // .............................................................. determine pixel depth

  MoveTo(10,70);
  DrawString("\pPixel depth = ");
  pixelDepth = (**pixMapHdl).pixelSize;
  NumToString((SInt32) pixelDepth,theString);
  DrawString(theString);

  // ..................................................... Get Device's Global Boundaries

  theRect = (**deviceHdl).gdRect;

  // ................................ determine bytes per row and total pixel image bytes

  MoveTo(10,90);
  bytesPerRow = (**pixMapHdl).rowBytes & 0x7FFF;
  DrawString("\pBytes per row = ");
  NumToString((SInt32) bytesPerRow,theString);
  DrawString(theString);

  MoveTo(10,105);
  DrawString("\pTotal pixel image bytes = ");
  NumToString((SInt32) bytesPerRow * theRect.bottom,theString);
  DrawString(theString);

  // ................. convert device's global boundaries to coordinates of graphics port

  GlobalToLocal(&topLeft(theRect));
  GlobalToLocal(&botRight(theRect));
  
  MoveTo(10,125);
  DrawString("\pBoundary rectangle top = ");
  NumToString((SInt32) theRect.top,theString);
  DrawString(theString);

  MoveTo(10,140);
  DrawString("\pBoundary rectangle left = ");
  NumToString((SInt32) theRect.left,theString);
  DrawString(theString);

  MoveTo(10,155);
  DrawString("\pBoundary rectangle bottom = ");
  NumToString((SInt32) theRect.bottom,theString);
  DrawString(theString);

  MoveTo(10,170);
  DrawString("\pBoundary rectangle right = ");
  NumToString((SInt32) theRect.right,theString);
  DrawString(theString);

  // ................................................ Get Pointer to Colour Graphics Port

  cgrafPtr = (CGrafPtr) windowPtr;

  // .............................................. determine requested background colour

  MoveTo(10,190);
  GetBackColor(&blueColour);
  DrawString("\pRequested background colour (rgb) = ");;
  MoveTo(10,205);
  NumToString((SInt32) blueColour.red,theString);
  DrawString(theString);
  DrawString("\p  ");
  NumToString((SInt32) blueColour.green,theString);
  DrawString(theString);
  DrawString("\p  ");
  NumToString((SInt32) blueColour.blue,theString);
  DrawString(theString);

  // .................................................... get actual colour (pixel value)

  pixelValue = cgrafPtr->bkColor;

  // ...... if direct device, extract colour components, else retrieve colour table index 

  MoveTo(10,220);

  if(deviceType == directType)
  {
    if(pixelDepth == 16)
    {
      redComponent    = pixelValue >> 10 & 0x0000001F;
      greenComponent  = pixelValue >> 5 & 0x0000001F;
      blueComponent    = pixelValue & 0x0000001F;
    }
    else if (pixelDepth == 32)
    {
      redComponent    = pixelValue >> 16 & 0x000000FF;
      greenComponent  = pixelValue >> 8 & 0x000000FF;
      blueComponent    = pixelValue & 0x000000FF;
    }

    DrawString("\pBackground colour used (rgb) = ");
    MoveTo(10,235);
    
    NumToString((SInt32) redComponent,theString);
    DrawString(theString);    
    DrawString("\p  ");

    NumToString((SInt32) greenComponent,theString);
    DrawString(theString);    
    DrawString("\p  ");

    NumToString((SInt32) blueComponent,theString);
    DrawString(theString);    
  }
  else if(deviceType == clutType || deviceType == fixedType)
  {
    DrawString("\p Background colour used (color table index) = ");    
    MoveTo(10,235);
    NumToString((SInt32) pixelValue,theString);
    DrawString(theString);
  }

  // ......................................................... Get Handle to Colour Table

  colorTableHdl = (*pixMapHdl)->pmTable;

  // ................................... if any entries in colour table, draw the colours

  MoveTo(250,20);
  DrawString("\pColour table in GDevice's PixMap:");

  entries = (*colorTableHdl)->ctSize;

  if(entries < 2)
  {
    MoveTo(260,105);
    DrawString("\pDummy (one entry) colour table only.");
    MoveTo(260,120);
    DrawString("\pTo get some entries, set the monitor to");
    MoveTo(260,135);
    DrawString("\p 256 colours, causing it to act like an");
    MoveTo(260,150);
    DrawString("\p                    indexed device.");
    SetRect(&theRect,250,28,458,236);
    FrameRect(&theRect);
  }

  for(a=28;a<224;a+=13)
  {
    for(b=250;b<446;b+=13)
    {
      if(c > entries)
        break;
      SetRect(&theRect,b,a,b+12,a+12);
      theColour = (*colorTableHdl)->ctTable[c++].rgb;
      RGBForeColor(&theColour);
      PaintRect(&theRect);
      if((deviceType == clutType || deviceType == fixedType) && c - 1 == pixelValue)
      {
        RGBForeColor(&whiteColour);
        InsetRect(&theRect,-1,-1);
        FrameRect(&theRect);
      }
    }
  }
}

//  doCheckMonitor

Boolean  doCheckMonitor(void)
{
  GDHandle      mainDeviceHdl;

  mainDeviceHdl = LMGetMainDevice();

  if(!(HasDepth(mainDeviceHdl,16,0,0)))
    return false;
  else
  {
    gStartupPixelDepth = (**((**mainDeviceHdl).gdPMap)).pixelSize;
    return true;
  }
}

//  doSetMonitorPixelDepth

void  doSetMonitorPixelDepth(void)
{
  GDHandle  mainDeviceHdl;
  Str255    alertString;  
  SInt16    pixelDepth;
  
  mainDeviceHdl = LMGetMainDevice();
  pixelDepth = (**((**mainDeviceHdl).gdPMap)).pixelSize;

  if(pixelDepth != 16)
  {
    GetIndString(alertString,rIndexedStrings,sSettingPixelDepth16);
    doMonitorAlert(alertString);
    SetDepth(mainDeviceHdl,16,0,0);
  }
  else
  {
    GetIndString(alertString,rIndexedStrings,sMonitorIsDepth16);
    doMonitorAlert(alertString);
  }
}

//  doRestoreMonitorPixelDepth

void  doRestoreMonitorPixelDepth(void)
{
  GDHandle  mainDeviceHdl;
  Str255    alertString;  
  SInt16    pixelDepth;

  mainDeviceHdl = LMGetMainDevice();
  pixelDepth = (**((**mainDeviceHdl).gdPMap)).pixelSize;

  if(pixelDepth != gStartupPixelDepth)
  {
    GetIndString(alertString,rIndexedStrings,sRestoringMonitor);
    doMonitorAlert(alertString);
    SetDepth(mainDeviceHdl,gStartupPixelDepth,0,0);
  }
  else
  {
    GetIndString(alertString,rIndexedStrings,sMonitorIsDepthStart);
    doMonitorAlert(alertString);
  }
}

//  doMonitorAlert

void  doMonitorAlert(Str255 labelText)
{
  AlertStdAlertParamRec paramRec;
  SInt16                itemHit;
  
  paramRec.movable        = true;
  paramRec.helpButton     = false;
  paramRec.filterProc     = NULL;
  paramRec.defaultText    = (StringPtr) kAlertDefaultOKText;
  paramRec.cancelText     = NULL;
  paramRec.otherText      = NULL;
  paramRec.defaultButton  = kAlertStdAlertOKButton;
  paramRec.cancelButton   = 0;
  paramRec.position       = kWindowDefaultPosition;

  StandardAlert(kAlertNoteAlert,labelText,NULL,¶mRec,&itemHit);
}

// 

Demonstration Program Comments

When this program is first run, the user should:

*   Drag the window to various position on the main screen, noting the changes to the
    coordinates of the boundary rectangle.

*   Open the Monitors and Sound control panel and, depending on the characteristics of
    the user's system:

*   Change between the available resolutions, noting the changes in the bytes per row
    and total pixel image bytes figures displayed in the window.

*   Change between the available colour depths, noting the changes to the pixel depth
    and total pixel image bytes figures, and the background colour used figures,
    displayed in the window.

*   Note that, when 256 or less colours are displayed on a direct device (in colours and
    grays), the device creates a CLUT and operates like a direct device.  In this case,
    the background colour used figure is the colour table entry (index), and the relevant
    colour in the colour table display is framed in white.

Assuming the user's monitor is a direct colour device, the user should then run the
program again with the monitor set to display 256 colours prior to program start.  The
Demonstration menu and its items will be enabled.  The user should then choose the items
in the Demonstration menu to set the monitor to a pixel depth of 16 and back to the
startup pixel depth.

main

Before DrawMenuBar is called, a call to the application-defined function doCheckMonitor
assigns the startup pixel depth to a global variable and determines whether the main
device supports 16-bit colour.  If the main device does not support 16-bit colour, the
Demonstration menu is disabled.  If the main device does support support 16-bit colour,
the Demonstration menu is disabled only if the current pixel depth is not 8 (256 colours)
or less.

doEvents

In the case of a mouse-down event, in the inDrag case, when the user releases the mouse
button, the left half of the window is invalidated, causing the left half to be redrawn
with the new boundary rectangle coordinates.

doDisplayInformation

In the first three lines, RGB colours are assigned to the window's colour graphics port's
rgbFgColor and rgbBkColor fields.  The call to EraseRect causes the content region to be
filled with the background colour.

Get Device List

The call to LMGetDeviceList gets a handle to the first GDevice structure in the device
list.  The device list is then "walked" in the while loop.  For every video device found
in the list, the variable videoDeviceCount is incremented.  GetNextDevice gets a handle
to the next device in the device list.

Get Main Device

LMGetMainDevice gets a handle to the startup device, that is, the device on which the
menu bar appears.

The call to BitTest with the gdDevType flag determines whether the main (startup) device
is a colour or black-and-white device.  In the next block, the gdType field of the
GDevice structure is examined to determine whether the device is an indexed device with a
variable CLUT, an indexed device with a fixed CLUT, or a direct device (or a direct
device set to display 256 colours or less and, as a consequence, acting like an indexed
device).

Get Handle to Pixel Map

At the first line of this block, a handle to the GDevice structure's pixel map is
retrieved from the gdPMap field.

In the next block, the pixel depth is extracted from the PixMap structure's pixelSize
field.

Get Device's Global Boundaries

At the first line of this block, the device's global boundaries are extracted from the
GDevice structure's gdRect field.

At the next block, the number of bytes in each row in the pixel map is determined.  (The
high bit in the rowBytes field of the PixMap structure is a flag which indicates whether
the data structure is a PixMap structure or a BitMap structure.)

At the next block, the bytes per row value is multiplied by the height of the boundary
rectangle to arrive at the total number of bytes in the pixel image.

The two calls to GlobalToLocal convert the boundary rectangle coordinates to coordinates
local to the colour graphics port.

Get Pointer To Colour Graphics Port

The first line simply casts the windowPtr to a pointer to a colour graphics port so that,
later on, the bkColor field can be accessed.

The next block gets the current (requested) background colour using the function
GetBackColor, and then extracts the red, green, and blue components.

At the next line, the pixel value in the bkColor field of the colour graphics port is
retrieved.  This is an SInt32 value holding either the red, green, and blue components of
the background colour actually used for drawing (direct device) or the colour table entry
used for drawing (indexed devices).

For direct devices with a pixel depth of 16, the first 15 bits hold the three RGB
components.  For direct devices with a pixel depth of 32, the first 24 bits hold the RGB
components.  These are extracted in the if(deviceType == directType) block.  For indexed
devices the value is simply the colour table entry (index) determined by the Color
Manager to represent the nearest match to the requested colour.

Get Handle To Colour Table

The first and fourth lines get a handle to the colour table in the GDevice structure's
pixel map and the number of entries in that table.

The final block paints small coloured rectangles for each entry in the colour table.  If
the main device is an indexed device (or if it is a direct device set to display 256
colours or less), the colour table entry being used as the best match for the requested
background colour is outlined in white.

doCheckMonitor

doCheckMonitor is called at program start to determine whether the main device supports
16-bit colour and, if it does, to assign the main device's pixel depth at startup to the
global variable gStartupPixelDepth.  

The call to LMGetMainDevice gets a handle to the main device's GDevice structure.  The
function HasDepth is used to determine whether the device supports 16-bit colour.  The
pixel depth is extracted from the pixelSize field of the PixMap structure in the GDevice
structure.

doSetMonitorPixelDepth

doSetMonitorPixelDepth is called when the first item in the Demonstration menu is chosen
to set the main device's pixel depth to 16.

If the current pixel depth determined at the first two lines is not 16, a string is
retrieved from a 'STR#' resource and passed to the application-defined function
doMonitorAlert, which displays a movable modal alert box advising the user that the
monitor's bit depth is about to be changed to 16.  When the user dismisses the alert box,
SetDepth sets the main device's pixel depth to 16.

If the current pixel depth is 16, the last two lines display an alert box advising the
user that the device is currently set to that pixel depth.

doRestoreMonitorPixelDepth

doRestoreMonitorPixelDepth is called when the second item in the Demonstration menu is
chosen to reset the main device's pixel depth to the startup pixel depth.

If the current pixel depth determined at the first two lines is not equal to the startup
pixel depth, a string is retrieved from a 'STR#' resource and passed to the
application-defined function doMonitorAlert, which displays a movable modal alert box
advising the user that the monitor's bit depth is about to be changed to the startup
pixel depth.  When the user dismisses the alert box, SetDepth sets the main device's
pixel depth to the startup pixel depth.

If the current pixel depth is the startup pixel depth, the last two lines display an
alert box advising the user that the device is currently set to that pixel depth.
 
MacTech Only Search:
Community Search:

 
 
 

 
 
 
 
 
  • SPREAD THE WORD:
  • Slashdot
  • Digg
  • Del.icio.us
  • Reddit
  • Newsvine
  • Generate a short URL for this page:



MacTech Magazine. www.mactech.com
Toll Free 877-MACTECH, Outside US/Canada: 805-494-9797
MacTech is a registered trademark of Xplain Corporation. Xplain, "The journal of Apple technology", Apple Expo, Explain It, MacDev, MacDev-1, THINK Reference, NetProfessional, Apple Expo, MacTech Central, MacTech Domains, MacNews, MacForge, and the MacTutorMan are trademarks or service marks of Xplain Corporation. Sprocket is a registered trademark of eSprocket Corporation. Other trademarks and copyrights appearing in this printing or software remain the property of their respective holders.
All contents are Copyright 1984-2010 by Xplain Corporation. All rights reserved. Theme designed by Icreon.
 
Nov. 20: Take Control of Syncing Data in Sow Leopard' released
Nov. 19: Cocktail 4.5 (Leopard Edition) released
Nov. 19: macProVideo offers new Cubase tutorials
Nov. 18: S Stardom anounces Safe Capsule, a companion piece for Apple's
Nov. 17: Ableton releases Max for Live
Nov. 17: Ableton releases Max for Live
Nov. 17: Ableton releases Max for Live
Nov. 17: Ableton releases Max for Live
Nov. 17: Ableton releases Max for Live
Nov. 17: Ableton releases Max for Live
Nov. 17: Ableton releases Max for Live
Nov. 17: Ableton releases Max for Live
Nov. 17: Ableton releases Max for Live
Nov. 17: Ableton releases Max for Live