static int _testNewPluginVst2xIdWithNullStringId(void)
{
    PluginVst2xId id = newPluginVst2xIdWithStringId(NULL);
    assertUnsignedLongEquals(ZERO_UNSIGNED_LONG, id->id);
    assertCharStringEquals(PLUGIN_VST2X_ID_UNKNOWN, id->idString);
    freePluginVst2xId(id);
    return 0;
}
static int _testNewPluginVst2xIdWithZeroIntId(void)
{
    PluginVst2xId id = newPluginVst2xIdWithId(0);
    assertUnsignedLongEquals(ZERO_UNSIGNED_LONG, id->id);
    assertCharStringEquals(EMPTY_STRING, id->idString);
    freePluginVst2xId(id);
    return 0;
}
static int _testNewPluginVst2xIdWithIntId(void)
{
    PluginVst2xId id = newPluginVst2xIdWithId(0x61626364);
    assertUnsignedLongEquals(0x61626364l, id->id);
    assertCharStringEquals("abcd", id->idString);
    freePluginVst2xId(id);
    return 0;
}
static int _testNewPluginVst2xIdWithInvalidStringId(void)
{
    CharString c = newCharStringWithCString("a");
    PluginVst2xId id = newPluginVst2xIdWithStringId(c);
    assertUnsignedLongEquals(ZERO_UNSIGNED_LONG, id->id);
    assertCharStringEquals(PLUGIN_VST2X_ID_UNKNOWN, id->idString);
    freePluginVst2xId(id);
    freeCharString(c);
    return 0;
}
static int _testNewPluginVst2xIdWithEmptyStringId(void)
{
    CharString empty = newCharStringWithCString(EMPTY_STRING);
    PluginVst2xId id = newPluginVst2xIdWithStringId(empty);
    assertUnsignedLongEquals(ZERO_UNSIGNED_LONG, id->id);
    assertCharStringEquals(PLUGIN_VST2X_ID_UNKNOWN, id->idString);
    freePluginVst2xId(id);
    freeCharString(empty);
    return 0;
}
static int _testNewPluginVst2xIdWithStringId(void)
{
    CharString c = newCharStringWithCString("abcd");
    PluginVst2xId id = newPluginVst2xIdWithStringId(c);
    assertUnsignedLongEquals(0x61626364l, id->id);
    assertCharStringEquals(c->data, id->idString);
    freePluginVst2xId(id);
    freeCharString(c);
    return 0;
}
VstIntPtr VSTCALLBACK pluginVst2xHostCallback(AEffect *effect, VstInt32 opcode, VstInt32 index, VstIntPtr value, void *dataPtr, float opt) {
  // This string is used in a bunch of logging calls below
  PluginVst2xId pluginId;
  if(effect != NULL) {
    pluginId = newPluginVst2xIdWithId(effect->uniqueID);
  }
  else {
    // During plugin initialization, the dispatcher can be called without a
    // valid plugin instance, as the AEffect* struct is still not fully constructed
    // at that point.
    pluginId = newPluginVst2xId();
  }
  const char* pluginIdString = pluginId->idString->data;
  VstIntPtr result = 0;

  logDebug("Plugin '%s' called host dispatcher with %d, %d, %d", pluginIdString, opcode, index, value);
  switch(opcode) {
    case audioMasterAutomate:
      // The plugin will call this if a parameter has changed via MIDI or the GUI, so the host can update
      // itself accordingly. We don't care about this (for the time being), and as we don't support either
      // GUI's or live MIDI, this opcode can be ignored.
      break;
    case audioMasterVersion:
      // We are a VST 2.4 compatible host
      result = 2400;
      break;
    case audioMasterCurrentId:
      // Use the current plugin ID, needed by VST shell plugins to determine which sub-plugin to load
      result = currentPluginUniqueId;
      break;
    case audioMasterIdle:
      // Ignore
      result = 1;
      break;
    case audioMasterPinConnected:
      logDeprecated("audioMasterPinConnected", pluginIdString);
      break;
    case audioMasterWantMidi:
      // This (deprecated) call is sometimes made by VST2.3 instruments to tell
      // the host that it is an instrument. We can safely ignore it.
      result = 1;
      break;
    case audioMasterGetTime: {
      AudioClock audioClock = getAudioClock();

      // These values are always valid
      vstTimeInfo.samplePos = audioClock->currentFrame;
      vstTimeInfo.sampleRate = getSampleRate();

      // Set flags for transport state
      vstTimeInfo.flags = 0;
      vstTimeInfo.flags |= audioClock->transportChanged ? kVstTransportChanged : 0;
      vstTimeInfo.flags |= audioClock->isPlaying ? kVstTransportPlaying : 0;

      // Fill values based on other flags which may have been requested
      if(value & kVstNanosValid) {
        // It doesn't make sense to return this value, as the plugin may try to calculate
        // something based on the current system time. As we are running offline, anything
        // the plugin calculates here will probably be wrong given the way we are running.
        // However, for realtime mode, this flag should be implemented in that case.
        logWarn("Plugin '%s' asked for time in nanoseconds (unsupported)", pluginIdString);
      }
      if(value & kVstPpqPosValid) {
        // TODO: Move calculations to AudioClock
        double samplesPerBeat = (60.0 / getTempo()) * getSampleRate();
        // Musical time starts with 1, not 0
        vstTimeInfo.ppqPos = (vstTimeInfo.samplePos / samplesPerBeat) + 1.0;
        logDebug("Current PPQ position is %g", vstTimeInfo.ppqPos);
        vstTimeInfo.flags |= kVstPpqPosValid;
      }
      if(value & kVstTempoValid) {
        vstTimeInfo.tempo = getTempo();
        vstTimeInfo.flags |= kVstTempoValid;
      }
      if(value & kVstBarsValid) {
        if(!(value & kVstPpqPosValid)) {
          logError("Plugin requested position in bars, but not PPQ");
        }
        // TODO: Move calculations to AudioClock
        double currentBarPos = floor(vstTimeInfo.ppqPos / (double)getTimeSignatureBeatsPerMeasure());
        vstTimeInfo.barStartPos = currentBarPos * (double)getTimeSignatureBeatsPerMeasure() + 1.0;
        logDebug("Current bar is %g", vstTimeInfo.barStartPos);
        vstTimeInfo.flags |= kVstBarsValid;
      }
      if(value & kVstCyclePosValid) {
        // We don't support cycling, so this is always 0
      }
      if(value & kVstTimeSigValid) {
        vstTimeInfo.timeSigNumerator = getTimeSignatureBeatsPerMeasure();
        vstTimeInfo.timeSigDenominator = getTimeSignatureNoteValue();
        vstTimeInfo.flags |= kVstTimeSigValid;
      }
      if(value & kVstSmpteValid) {
        logUnsupportedFeature("Current time in SMPTE format");
      }
      if(value & kVstClockValid) {
        logUnsupportedFeature("Sample frames until next clock");
      }

      result = (VstIntPtr)&vstTimeInfo;
      break;
    }
    case audioMasterProcessEvents:
      logUnsupportedFeature("VST master opcode audioMasterProcessEvents");
      break;
    case audioMasterSetTime:
      logDeprecated("audioMasterSetTime", pluginIdString);
      break;
    case audioMasterTempoAt:
      logDeprecated("audioMasterTempoAt", pluginIdString);
      break;
    case audioMasterGetNumAutomatableParameters:
      logDeprecated("audioMasterGetNumAutomatableParameters", pluginIdString);
      break;
    case audioMasterGetParameterQuantization:
      logDeprecated("audioMasterGetParameterQuantization", pluginIdString);
      break;
    case audioMasterIOChanged: {
      PluginChain pluginChain = getPluginChain();
      logDebug("Number of inputs: %d", effect->numInputs);
      logDebug("Number of outputs: %d", effect->numOutputs);
      logDebug("Number of parameters: %d", effect->numParams);
      logDebug("Initial Delay: %d", effect->initialDelay);
      result = -1;
      for(unsigned int i = 0; i < pluginChain->numPlugins; ++i){
        if((unsigned long)effect->uniqueID == pluginVst2xGetUniqueId(pluginChain->plugins[i])){
          logDebug("Updating plugin");
          pluginVst2xAudioMasterIOChanged(pluginChain->plugins[i], effect);
          result = 0;
          break;//Only one plugin will match anyway.
        }
      }
      break;
    }
    case audioMasterNeedIdle:
      logDeprecated("audioMasterNeedIdle", pluginIdString);
      break;
    case audioMasterSizeWindow:
      logWarn("Plugin '%s' asked us to resize window (unsupported)", pluginIdString);
      break;
    case audioMasterGetSampleRate:
      result = (int)getSampleRate();
      break;
    case audioMasterGetBlockSize:
      result = getBlocksize();
      break;
    case audioMasterGetInputLatency:
      // Input latency is not used, and is always 0
      result = 0;
      break;
    case audioMasterGetOutputLatency:
      // Output latency is not used, and is always 0
      result = 0;
      break;
    case audioMasterGetPreviousPlug:
      logDeprecated("audioMasterGetPreviousPlug", pluginIdString);
      break;
    case audioMasterGetNextPlug:
      logDeprecated("audioMasterGetNextPlug", pluginIdString);
      break;
    case audioMasterWillReplaceOrAccumulate:
      logDeprecated("audioMasterWillReplaceOrAccumulate", pluginIdString);
      break;
    case audioMasterGetCurrentProcessLevel:
      // We are not a multithreaded app and have no GUI, so this is unsupported.
      result = kVstProcessLevelUnknown;
      break;
    case audioMasterGetAutomationState:
      // Automation is also not supported (for now)
      result = kVstAutomationUnsupported;
      break;
    case audioMasterOfflineStart:
      logWarn("Plugin '%s' asked us to start offline processing (unsupported)", pluginIdString);
      break;
    case audioMasterOfflineRead:
      logWarn("Plugin '%s' asked to read offline data (unsupported)", pluginIdString);
      break;
    case audioMasterOfflineWrite:
      logWarn("Plugin '%s' asked to write offline data (unsupported)", pluginIdString);
      break;
    case audioMasterOfflineGetCurrentPass:
      logWarn("Plugin '%s' asked for current offline pass (unsupported)", pluginIdString);
      break;
    case audioMasterOfflineGetCurrentMetaPass:
      logWarn("Plugin '%s' asked for current offline meta pass (unsupported)", pluginIdString);
      break;
    case audioMasterSetOutputSampleRate:
      logDeprecated("audioMasterSetOutputSampleRate", pluginIdString);
      break;
    case audioMasterGetOutputSpeakerArrangement:
      logDeprecated("audioMasterGetOutputSpeakerArrangement", pluginIdString);
      break;
    case audioMasterGetVendorString:
      strncpy((char*)dataPtr, VENDOR_NAME, kVstMaxVendorStrLen);
      result = 1;
      break;
    case audioMasterGetProductString:
      strncpy((char*)dataPtr, PROGRAM_NAME, kVstMaxProductStrLen);
      result = 1;
      break;
    case audioMasterGetVendorVersion:
      // Return our version as a single string, in the form ABCC, which corresponds to version A.B.C
      // Often times the patch can reach double-digits, so it gets two decimal places.
      result = VERSION_MAJOR * 1000 + VERSION_MINOR * 100 + VERSION_PATCH;
      break;
    case audioMasterVendorSpecific:
      logWarn("Plugin '%s' made a vendor specific call (unsupported). Arguments: %d, %d, %f", pluginIdString, index, value, opt);
      break;
    case audioMasterCanDo:
      result = _canHostDo(pluginIdString, (char*)dataPtr);
      break;
    case audioMasterSetIcon:
      logDeprecated("audioMasterSetIcon", pluginIdString);
      break;
    case audioMasterGetLanguage:
      result = kVstLangEnglish;
      break;
    case audioMasterOpenWindow:
      logDeprecated("audioMasterOpenWindow", pluginIdString);
      break;
    case audioMasterCloseWindow:
      logDeprecated("audioMasterCloseWindow", pluginIdString);
      break;
    case audioMasterGetDirectory:
      logWarn("Plugin '%s' asked for directory pointer (unsupported)", pluginIdString);
      break;
    case audioMasterUpdateDisplay:
      // Ignore
      break;
    case audioMasterBeginEdit:
      logWarn("Plugin '%s' asked to begin parameter automation (unsupported)", pluginIdString);
      break;
    case audioMasterEndEdit:
      logWarn("Plugin '%s' asked to end parameter automation (unsupported)", pluginIdString);
      break;
    case audioMasterOpenFileSelector:
      logWarn("Plugin '%s' asked us to open file selector (unsupported)", pluginIdString);
      break;
    case audioMasterCloseFileSelector:
      logWarn("Plugin '%s' asked us to close file selector (unsupported)", pluginIdString);
      break;
    case audioMasterEditFile:
      logDeprecated("audioMasterEditFile", pluginIdString);
      break;
    case audioMasterGetChunkFile:
      logDeprecated("audioMasterGetChunkFile", pluginIdString);
      break;
    case audioMasterGetInputSpeakerArrangement:
      logDeprecated("audioMasterGetInputSpeakerArrangement", pluginIdString);
      break;
    default:
      logWarn("Plugin '%s' asked if host can do unknown opcode %d", pluginIdString, opcode);
      break;
  }

  freePluginVst2xId(pluginId);
  return result;
}