diff --git a/juce_amalgamated.cpp b/juce_amalgamated.cpp index ea36eaf025..f4d6c25d32 100644 --- a/juce_amalgamated.cpp +++ b/juce_amalgamated.cpp @@ -11974,6 +11974,11 @@ const String String::dropLastCharacters (const int numberToDrop) const throw() jmax (0, CharacterFunctions::length (text->text) - numberToDrop)); } +const String String::getLastCharacters (const int numCharacters) const throw() +{ + return String (text->text + jmax (0, CharacterFunctions::length (text->text) - jmax (0, numCharacters))); +} + const String String::fromFirstOccurrenceOf (const tchar* const sub, const bool includeSubString, const bool ignoreCase) const throw() @@ -261137,6 +261142,637 @@ bool WebBrowserComponent::pageAboutToLoad (const String& url) // compiled on its own). #ifdef JUCE_INCLUDED_FILE +@interface UIKitAUIOHost : UIViewController +{ +@public + /** READONLY The audio format of the data stream. */ + AudioStreamBasicDescription format; + AURenderCallbackStruct inputProc; + Float64 hwSampleRate; + AudioUnit rioUnit; + UGen rawInput; + UGen postFadeOutput; + UGen preFadeOutput; + int bufferSize; + float *floatBuffer; + UInt32 audioInputIsAvailable; + UInt32 numInputChannels; + UInt32 numOutputChannels; + bool isRunning; + float fadeInTime; + UGenArray others; + NSLock* nsLock; +} + +/** Initialises the AudioUnit framework and structures. + Do not call this method, it is called automatically when the application launches. */ +- (void)initAudio; + +/** Construct a UGen graph. + You must implement this in your subclass. You should return a UGen which will be the UGen graph which is + performed and rendered to the host. The input parameter may be ignored if only signal generation is required + or may be used if a processing algorithm is being implemented (e.g., filtering incoming audio data). + + @param input The input UGen which will contain audio data from the host. + @return the UGen graph which will be performed */ +- (UGen)constructGraph:(UGen)input; + +- (void)addOther:(UGen)ugen; + +- (void)lock; +- (void)unlock; +- (BOOL)tryLock; + +@end + +#define NUM_CHANNELS 2 + +void SetFormat(AudioStreamBasicDescription& format) +{ + memset(&format, 0, sizeof(AudioStreamBasicDescription)); + format.mFormatID = kAudioFormatLinearPCM; + int sampleSize = sizeof(AudioSampleType); + format.mFormatFlags = kAudioFormatFlagsCanonical; + format.mBitsPerChannel = 8 * sampleSize; + format.mChannelsPerFrame = NUM_CHANNELS; + format.mFramesPerPacket = 1; + format.mBytesPerPacket = format.mBytesPerFrame = sampleSize; + format.mFormatFlags |= kAudioFormatFlagIsNonInterleaved; +} + +int SetupRemoteIO (AudioUnit& inRemoteIOUnit, AURenderCallbackStruct inRenderProc, AudioStreamBasicDescription& outFormat) +{ + // Open the output unit + AudioComponentDescription desc; + desc.componentType = kAudioUnitType_Output; + desc.componentSubType = kAudioUnitSubType_RemoteIO; + desc.componentManufacturer = kAudioUnitManufacturer_Apple; + desc.componentFlags = 0; + desc.componentFlagsMask = 0; + + AudioComponent comp = AudioComponentFindNext (NULL, &desc); + AudioComponentInstanceNew (comp, &inRemoteIOUnit); + + const UInt32 one = 1; + AudioUnitSetProperty(inRemoteIOUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &one, sizeof(one)); + AudioUnitSetProperty(inRemoteIOUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &inRenderProc, sizeof(inRenderProc)); + + AudioUnitSetProperty(inRemoteIOUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &outFormat, sizeof(outFormat)); + AudioUnitSetProperty(inRemoteIOUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &outFormat, sizeof(outFormat)); + + AudioUnitInitialize(inRemoteIOUnit); + + return 0; +} + +static const float FloatToFixed824_Factor = 16777216.f; +static const float Fixed824ToFloat_Factor = 5.960464477539e-08f; + +static const float FloatToPCM16Bit_Factor = 32767.f; +static const float PCM16BitToFloat_Factor = 3.051850947600e-05f; + +static OSStatus PerformThru(void *inRefCon, + AudioUnitRenderActionFlags *ioActionFlags, + const AudioTimeStamp *inTimeStamp, + UInt32 inBusNumber, + UInt32 inNumberFrames, + AudioBufferList *ioData) +{ + OSStatus err = 0; + UIKitAUIOHost *x = (UIKitAUIOHost *)inRefCon; + + [x lock]; + + if(x->audioInputIsAvailable) + { + err = AudioUnitRender(x->rioUnit, ioActionFlags, inTimeStamp, 1, inNumberFrames, ioData); + if (err) { printf("PerformThru: error %d\n", (int)err); return err; } + } + + if(inNumberFrames > x->bufferSize) + { + delete [] x->floatBuffer; + x->bufferSize = inNumberFrames; + + x->floatBuffer = new float[inNumberFrames * NUM_CHANNELS]; + } + + long blockID = UGen::getNextBlockID(inNumberFrames); + + float *floatBufferData[2]; + floatBufferData[0] = x->floatBuffer; + floatBufferData[1] = floatBufferData[0] + inNumberFrames; + + if(x->audioInputIsAvailable) + { + for (UInt32 channel = 0; channel < x->numInputChannels; channel++) + { + AudioSampleType *audioUnitBuffer = (AudioSampleType*)ioData->mBuffers[0].mData; + float *floatBuffer = floatBufferData[channel]; + + for(int sample = 0; sample < inNumberFrames; sample++) + { + floatBuffer[sample] = (float)audioUnitBuffer[sample] * PCM16BitToFloat_Factor; + } + } + + x->rawInput.getSource().setInputs((const float**)floatBufferData, inNumberFrames, x->numInputChannels); + } + else + { + memset(x->floatBuffer, 0, x->numInputChannels * inNumberFrames * sizeof(float)); + } + + x->postFadeOutput.setOutputs(floatBufferData, inNumberFrames, 2); + x->postFadeOutput.prepareAndProcessBlock(inNumberFrames, blockID); + + for (UInt32 channel = 0; channel < ioData->mNumberBuffers; channel++) + { + AudioSampleType *audioUnitBuffer = (AudioSampleType*)ioData->mBuffers[channel].mData; + float *floatBuffer = floatBufferData[channel]; + + for(int sample = 0; sample < inNumberFrames; sample++) + { + audioUnitBuffer[sample] = (AudioSampleType)(floatBuffer[sample] * FloatToPCM16Bit_Factor); + } + } + + for(int i = 0; i < x->others.size(); i++) + { + x->others[i].prepareAndProcessBlock(inNumberFrames, blockID); + } + + [x unlock]; + + return err; +} + +void propListener(void * inClientData, + AudioSessionPropertyID inID, + UInt32 inDataSize, + const void * inPropertyValue) +{ + printf("Property changed!\n"); + + UIKitAUIOHost *x = (UIKitAUIOHost *)inClientData; + + if(!x->isRunning) return; + + if(inPropertyValue) + { + CFDictionaryRef routeChangeDictionary = (CFDictionaryRef)inPropertyValue; + CFNumberRef routeChangeReasonRef = + (CFNumberRef)CFDictionaryGetValue (routeChangeDictionary, + CFSTR (kAudioSession_AudioRouteChangeKey_Reason)); + + SInt32 routeChangeReason; + CFNumberGetValue(routeChangeReasonRef, kCFNumberSInt32Type, &routeChangeReason); + + CFStringRef newAudioRoute; + UInt32 propertySize = sizeof (CFStringRef); + AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &propertySize, &newAudioRoute); + + printf("route=%s\n", CFStringGetCStringPtr(newAudioRoute, CFStringGetSystemEncoding())); + + } + + UInt32 size = sizeof(UInt32); + AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareInputNumberChannels, &size, &x->numInputChannels); + AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareOutputNumberChannels, &size, &x->numOutputChannels); + AudioSessionGetProperty(kAudioSessionProperty_AudioInputAvailable, &size, &x->audioInputIsAvailable); + + printf("inputs=%d outputs=%d audioInputIsAvailable=%d\n", x->numInputChannels, x->numOutputChannels, x->audioInputIsAvailable); + + if(x->rioUnit) + { + AudioComponentInstanceDispose(x->rioUnit); + } + + SetFormat(x->format); + SetupRemoteIO(x->rioUnit, x->inputProc, x->format); + + x->rawInput.setSource(AudioIn::AR(x->numInputChannels), true); + x->postFadeOutput = Plug::AR(UGen::emptyChannels(x->preFadeOutput.getNumChannels())); + x->postFadeOutput.fadeSourceAndRelease(x->preFadeOutput, x->fadeInTime); + + AudioSessionSetActive(true); + AudioOutputUnitStart(x->rioUnit); +} + +void rioInterruptionListener(void *inClientData, UInt32 inInterruption) +{ + printf("Session interrupted! --- %s ---\n", inInterruption == kAudioSessionBeginInterruption ? "Begin Interruption" : "End Interruption"); + + UIKitAUIOHost *x = (UIKitAUIOHost *)inClientData; + + if (inInterruption == kAudioSessionEndInterruption) { + // make sure we are again the active session + //AudioSessionSetActive(false); + AudioSessionSetActive(true); + x->isRunning = true; + AudioOutputUnitStart(x->rioUnit); + } + + if (inInterruption == kAudioSessionBeginInterruption) { + x->isRunning = false; + AudioOutputUnitStop(x->rioUnit); + + printf("rioInterruptionListener audioInputIsAvailable=%d\n", x->audioInputIsAvailable); + + UIAlertView *baseAlert = [[UIAlertView alloc] initWithTitle:@"Audio interrupted" + message:@"This could have been interrupted by another application or due to unplugging a headset:" + delegate:x + cancelButtonTitle:nil + otherButtonTitles:@"Resume", @"Cancel", nil]; + [baseAlert show]; + } + +} + +@implementation UIKitAUIOHost + +- (id)init +{ + if (self = [super init]) + { + nsLock = [[NSLock alloc] init]; + fadeInTime = 1.0; + [self performSelector:@selector(initAudio) withObject:nil afterDelay:1.0]; + } + return self; +} + +- (void)initAudio +{ + // render proc + inputProc.inputProc = PerformThru; + inputProc.inputProcRefCon = self; + + // session + AudioSessionInitialize (NULL, NULL, rioInterruptionListener, self); + AudioSessionSetActive (true); + + UInt32 audioCategory = kAudioSessionCategory_PlayAndRecord; + AudioSessionSetProperty(kAudioSessionProperty_AudioCategory, sizeof(audioCategory), &audioCategory); + AudioSessionAddPropertyListener(kAudioSessionProperty_AudioRouteChange, propListener, self); + + UInt32 size = sizeof(hwSampleRate); + AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareSampleRate, &size, &hwSampleRate); + + Float32 bufferDuration = 512 / hwSampleRate; + AudioSessionSetProperty(kAudioSessionProperty_PreferredHardwareIOBufferDuration, sizeof(bufferDuration), &bufferDuration); + + UGen::initialise(); + UGen::prepareToPlay(hwSampleRate, 512); + + rawInput = Plug::AR(UGen::emptyChannels(2)); + preFadeOutput = [self constructGraph: rawInput]; + + rioUnit = NULL; + isRunning = true; + propListener((void*)self, 0,0,0); + + size = sizeof(format); + AudioUnitGetProperty(rioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &format, &size); + + //Float32 bufferDuration; + size = sizeof(bufferDuration); + AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareIOBufferDuration, &size, &bufferDuration); + + bufferSize = (int)(hwSampleRate*bufferDuration+0.5); + floatBuffer = new float[bufferSize * NUM_CHANNELS]; +} + +- (UGen)constructGraph:(UGen)input +{ + return UGen::emptyChannels(NUM_CHANNELS); +} + +- (void)addOther:(UGen)ugen +{ + [self lock]; + others <<= ugen; + [self unlock]; +} + +- (void)lock +{ + [nsLock lock]; +} + +- (void)unlock +{ + [nsLock unlock]; +} + +- (BOOL)tryLock +{ + return [nsLock tryLock]; +} + +- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex +{ + printf("buttonIndex=%d\n", buttonIndex); + + if(buttonIndex == 0) + { + // resume + isRunning = true; + propListener((void*)self, 0,0,0); + } + + [alertView release]; +} + +-(void) dealloc +{ + UGen::shutdown(); + delete [] floatBuffer; + [nsLock release]; + [super dealloc]; +} +@end + +class IPhoneAudioIODevice : public AudioIODeviceType +{ +public: + + IPhoneAudioIODevice (const String& deviceName, const bool isInput_) + : AudioIODevice (deviceName, T("Audio")), + isInput (isInput_), + isOpen_ (false), + isStarted (false) + { + } + + ~IPhoneAudioIODeviceType() + { + } + + const StringArray getOutputChannelNames() + { + StringArray s; + if (! isInput) + { + s.add ("Left"); + s.add ("Right"); + } + return s; + } + + const StringArray getInputChannelNames() + { + StringArray s; + if (isInput) + { + s.add ("Left"); + s.add ("Right"); + } + return s; + } + + int getNumSampleRates() + { + return sampleRates.size(); + } + + double getSampleRate (int index) + { + return sampleRates [index]; + } + + int getNumBufferSizesAvailable() + { + return bufferSizes.size(); + } + + int getBufferSizeSamples (int index) + { + return bufferSizes [index]; + } + + int getDefaultBufferSize() + { + for (int i = 0; i < getNumBufferSizesAvailable(); ++i) + if (getBufferSizeSamples(i) >= 512) + return getBufferSizeSamples(i); + + return 512; + } + + const String open (const BitArray& inputChannels, + const BitArray& outputChannels, + double sampleRate, + int bufferSizeSamples) + { + isOpen_ = true; + + if (bufferSizeSamples <= 0) + bufferSizeSamples = getDefaultBufferSize(); + + lastError = String::empty; + + isOpen_ = lastError.isEmpty(); + return lastError; + } + + void close() + { + isOpen_ = false; + + } + + bool isOpen() + { + return isOpen_; + } + + int getCurrentBufferSizeSamples() + { + return internal != 0 ? internal->getBufferSize() : 512; + } + + double getCurrentSampleRate() + { + return internal != 0 ? internal->getSampleRate() : 0; + } + + int getCurrentBitDepth() + { + return 32; // no way to find out, so just assume it's high.. + } + + const BitArray getActiveOutputChannels() const + { + return internal != 0 ? internal->activeOutputChans : BitArray(); + } + + const BitArray getActiveInputChannels() const + { + BitArray chans; + + if (internal != 0) + { + chans = internal->activeInputChans; + + if (internal->inputDevice != 0) + chans.orWith (internal->inputDevice->activeInputChans); + } + + return chans; + } + + int getOutputLatencyInSamples() + { + if (internal == 0) + return 0; + + // this seems like a good guess at getting the latency right - comparing + // this with a round-trip measurement, it gets it to within a few millisecs + // for the built-in mac soundcard + return internal->outputLatency + internal->getBufferSize() * 2; + } + + int getInputLatencyInSamples() + { + if (internal == 0) + return 0; + + return internal->inputLatency + internal->getBufferSize() * 2; + } + + void start (AudioIODeviceCallback* callback) + { + if (internal != 0 && ! isStarted) + { + if (callback != 0) + callback->audioDeviceAboutToStart (this); + + isStarted = true; + internal->start (callback); + } + } + + void stop() + { + if (isStarted && internal != 0) + { + AudioIODeviceCallback* const lastCallback = internal->callback; + + isStarted = false; + internal->stop (true); + + if (lastCallback != 0) + lastCallback->audioDeviceStopped(); + } + } + + bool isPlaying() + { + if (internal->callback == 0) + isStarted = false; + + return isStarted; + } + + const String getLastError() + { + return lastError; + } + + int inputIndex, outputIndex; + + juce_UseDebuggingNewOperator + +private: + CoreAudioInternal* internal; + bool isOpen_, isStarted; + String lastError; + + static OSStatus hardwareListenerProc (AudioDeviceID /*inDevice*/, UInt32 /*inLine*/, const AudioObjectPropertyAddress* pa, void* inClientData) + { + CoreAudioInternal* const intern = (CoreAudioInternal*) inClientData; + + switch (pa->mSelector) + { + case kAudioHardwarePropertyDevices: + intern->deviceDetailsChanged(); + break; + + case kAudioHardwarePropertyDefaultOutputDevice: + case kAudioHardwarePropertyDefaultInputDevice: + case kAudioHardwarePropertyDefaultSystemOutputDevice: + break; + } + + return noErr; + } + + CoreAudioIODevice (const CoreAudioIODevice&); + const CoreAudioIODevice& operator= (const CoreAudioIODevice&); +}; + +class IPhoneAudioIODeviceType : public AudioIODeviceType +{ +public: + + IPhoneAudioIODeviceType() + : AudioIODeviceType (T("iPhone Audio")), + hasScanned (false) + { + } + + ~IPhoneAudioIODeviceType() + { + } + + void scanForDevices() + { + } + + const StringArray getDeviceNames (const bool wantInputNames) const + { + StringArray s; + return s; + } + + int getDefaultDeviceIndex (const bool forInput) const + { + return 0; + } + + int getIndexOfDevice (AudioIODevice* device, const bool asInput) const + { + return 0; + } + + bool hasSeparateInputsAndOutputs() const { return true; } + + AudioIODevice* createDevice (const String& outputDeviceName, + const String& inputDeviceName) + { + if (outputDeviceName.isNotEmpty() && inputDeviceName.isNotEmpty()) + return new CoreAudioIODevice (deviceName, + inputIds [inputIndex], + inputIndex, + outputIds [outputIndex], + outputIndex); + + return 0; + } + + juce_UseDebuggingNewOperator + +private: + IPhoneAudioIODeviceType (const IPhoneAudioIODeviceType&); + const IPhoneAudioIODeviceType& operator= (const IPhoneAudioIODeviceType&); +}; + +AudioIODeviceType* juce_createAudioIODeviceType_iPhoneAudio() +{ + return new IPhoneAudioIODeviceType(); +} + #endif /********* End of inlined file: juce_iphone_Audio.cpp *********/ diff --git a/juce_amalgamated.h b/juce_amalgamated.h index b8434159c3..48ccc55449 100644 --- a/juce_amalgamated.h +++ b/juce_amalgamated.h @@ -1905,7 +1905,7 @@ public: @param startIndex the index of the start of the substring needed @param endIndex all characters from startIndex up to (but not including) this index are returned - @see fromFirstOccurrenceOf, dropLastCharacters, upToFirstOccurrenceOf + @see fromFirstOccurrenceOf, dropLastCharacters, getLastCharacters, upToFirstOccurrenceOf */ const String substring (int startIndex, int endIndex) const throw(); @@ -1916,7 +1916,7 @@ public: of the string, an empty string is returned. If it is zero or less, the whole string is returned. @returns the substring from startIndex up to the end of the string - @see dropLastCharacters, fromFirstOccurrenceOf, upToFirstOccurrenceOf, fromLastOccurrenceOf + @see dropLastCharacters, getLastCharacters, fromFirstOccurrenceOf, upToFirstOccurrenceOf, fromLastOccurrenceOf */ const String substring (const int startIndex) const throw(); @@ -1931,6 +1931,15 @@ public: */ const String dropLastCharacters (const int numberToDrop) const throw(); + /** Returns a number of characters from the end of the string. + + This returns the last numCharacters characters from the end of the string. If the + string is shorter than numCharacters, the whole string is returned. + + @see substring, dropLastCharacters, getLastCharacter + */ + const String getLastCharacters (const int numCharacters) const throw(); + /** Returns a section of the string starting from a given substring. This will search for the first occurrence of the given substring, and @@ -1982,6 +1991,7 @@ public: /** Returns the start of this string, up to the last occurrence of a substring. Similar to upToFirstOccurrenceOf(), but this finds the last occurrence rather than the first. + If the substring isn't found, this will return an empty string. @see upToFirstOccurrenceOf, fromFirstOccurrenceOf */ @@ -53543,9 +53553,8 @@ private: #ifndef __JUCE_QUICKTIMEMOVIECOMPONENT_JUCEHEADER__ #define __JUCE_QUICKTIMEMOVIECOMPONENT_JUCEHEADER__ -// this is used to disable QuickTime, and is defined in juce_Config.h -#if JUCE_QUICKTIME || DOXYGEN - +// (NB: This stuff mustn't go inside the "#if QUICKTIME" block, or it'll break the +// amalgamated build) #if JUCE_WINDOWS /********* Start of inlined file: juce_ActiveXControlComponent.h *********/ @@ -53660,6 +53669,9 @@ private: typedef NSViewComponent QTCompBaseClass; #endif +// this is used to disable QuickTime, and is defined in juce_Config.h +#if JUCE_QUICKTIME || DOXYGEN + /** A window that can play back a QuickTime movie. diff --git a/src/gui/components/special/juce_QuickTimeMovieComponent.h b/src/gui/components/special/juce_QuickTimeMovieComponent.h index dbb0d6168e..11cc1c427e 100644 --- a/src/gui/components/special/juce_QuickTimeMovieComponent.h +++ b/src/gui/components/special/juce_QuickTimeMovieComponent.h @@ -28,9 +28,8 @@ #include "../../../io/files/juce_File.h" -// this is used to disable QuickTime, and is defined in juce_Config.h -#if JUCE_QUICKTIME || DOXYGEN - +// (NB: This stuff mustn't go inside the "#if QUICKTIME" block, or it'll break the +// amalgamated build) #if JUCE_WINDOWS #include "juce_ActiveXControlComponent.h" typedef ActiveXControlComponent QTCompBaseClass; @@ -39,6 +38,9 @@ typedef NSViewComponent QTCompBaseClass; #endif +// this is used to disable QuickTime, and is defined in juce_Config.h +#if JUCE_QUICKTIME || DOXYGEN + //============================================================================== /** A window that can play back a QuickTime movie. diff --git a/src/text/juce_String.cpp b/src/text/juce_String.cpp index 18e30fe81a..52ac77be27 100644 --- a/src/text/juce_String.cpp +++ b/src/text/juce_String.cpp @@ -1595,6 +1595,11 @@ const String String::dropLastCharacters (const int numberToDrop) const throw() jmax (0, CharacterFunctions::length (text->text) - numberToDrop)); } +const String String::getLastCharacters (const int numCharacters) const throw() +{ + return String (text->text + jmax (0, CharacterFunctions::length (text->text) - jmax (0, numCharacters))); +} + const String String::fromFirstOccurrenceOf (const tchar* const sub, const bool includeSubString, const bool ignoreCase) const throw() diff --git a/src/text/juce_String.h b/src/text/juce_String.h index 6aeb28b3f0..b274821f54 100644 --- a/src/text/juce_String.h +++ b/src/text/juce_String.h @@ -510,7 +510,7 @@ public: @param startIndex the index of the start of the substring needed @param endIndex all characters from startIndex up to (but not including) this index are returned - @see fromFirstOccurrenceOf, dropLastCharacters, upToFirstOccurrenceOf + @see fromFirstOccurrenceOf, dropLastCharacters, getLastCharacters, upToFirstOccurrenceOf */ const String substring (int startIndex, int endIndex) const throw(); @@ -521,7 +521,7 @@ public: of the string, an empty string is returned. If it is zero or less, the whole string is returned. @returns the substring from startIndex up to the end of the string - @see dropLastCharacters, fromFirstOccurrenceOf, upToFirstOccurrenceOf, fromLastOccurrenceOf + @see dropLastCharacters, getLastCharacters, fromFirstOccurrenceOf, upToFirstOccurrenceOf, fromLastOccurrenceOf */ const String substring (const int startIndex) const throw(); @@ -536,6 +536,15 @@ public: */ const String dropLastCharacters (const int numberToDrop) const throw(); + /** Returns a number of characters from the end of the string. + + This returns the last numCharacters characters from the end of the string. If the + string is shorter than numCharacters, the whole string is returned. + + @see substring, dropLastCharacters, getLastCharacter + */ + const String getLastCharacters (const int numCharacters) const throw(); + //============================================================================== /** Returns a section of the string starting from a given substring. @@ -588,6 +597,7 @@ public: /** Returns the start of this string, up to the last occurrence of a substring. Similar to upToFirstOccurrenceOf(), but this finds the last occurrence rather than the first. + If the substring isn't found, this will return an empty string. @see upToFirstOccurrenceOf, fromFirstOccurrenceOf */