Pitch Detection in iOS 4.x
Pitch detection is a relatively common thing to do in the audio realm. After scouring the web I found a lot of useful Core Audio resources, but nothing directly related to pitch detection. In my searching I also found others who seemed to be looking for a way to do this. I decided to write a little app which demonstrates how pitch detection can be accomplished in iOS.
This is my first real tutorial so feedback is greatly appreciated. If you find this tutorial useful, let me know!
Cheers,
Demetri
A few things before we get started:
- I’m assuming readers are somewhat familiar with Xcode and iOS development. If you are just learning the ropes, this tutorial may not be the best place to start. Apple has plenty of resources on their developer’s website to get started.
- You need iOS 4.0 or higher to use this application because I use the new iOS Accelerate framework’s FFT functions for frequency analysis. You can download the latest SDK from Apple’s iOS Dev Center.
- To keep the tutorial size from exploding, I’ve skimmed over some of the CoreAudio details and tried to focus on the pitch detection piece. If there is confusion with my code, let me know and I’ll elaborate.
- I have stripped out some functionality of the app for clarity’s sake. I tried to remove most references to old code/comments though it’s possible I may have missed a few references.
- Finally, I couldn’t have completed this app without the help from Michael Tyson’s post on RemoteI/O. If you want a better understanding of different parts of Core Audio, I highly recommend checking out his blog post on the topic: http://atastypixel.com/blog/using-remoteio-audio-unit/
- Disclaimer 1: I know I glazed over a lot of details for the signal analysis portion of this application. It was never my intention for this tutorial to be the “end-all be-all” for signal analysis and pitch detection, but rather a foothold for those who want to learn to get started.
- Disclaimer 2: This application is a good proof of concept but if you are planning on writing a tuner application for the iPhone and releasing it to the public, you’ll want to refine your frequency analysis algorithm (among other things).
EDIT: I’ve moved PitchDetector to github and also fixed a bug that is reproducible only on the device. Follow the link below to download/checkout the source code.
Note: Below is a link to the source code on github.com. The code is commented generously. If you have questions, let me know in the comments and I’ll elaborate as soon as possible.
Click here to view PitchDetector project page
Pitch Detector
- Application overview: The application uses the CoreAudio Remote I/O API to sample and analyze input signals from an iOS devices microphone. The resulting frequency is then displayed on screen.
- I’ll be walking through the Pitch Detector application lifecycle from the point a user clicks the “Begin Listening” button. Elaborating on certain pieces of the application as they are encountered.
Now without further ado…
Listener Controls
The “Begin Listening” button is linked to a toggle method which starts and stops the listener (In this context, listener is an entity which listens for audio signals).
Below is the startListener method (see ListenerViewController – [line 37]):
- (void)startListener { [rioRef startListening:self]; } |
rioRef is ListenerViewController’s reference to RIOInterface (see ListenerViewController.h). RIOInterface (read: Remote I/O interface) is the bread and butter of this application. All the CoreAudio components needed are contained within this class. RIOInterface is singleton class since we will only ever be dealing with a single Remote I/O Audio Unit…more on that later.
RIOInterface.mm
Let’s look at RIOInterface’s - (void)startListening:(ListenerViewController*)aListener method (RIOInterface.mm – [line 62]):
- (void)startListening:(ListenerViewController*)aListener { self.listener = aListener; [self createAUProcessingGraph]; [self initializeAndStartProcessingGraph]; } |
The two important methods here are [self createAUProcessingGraph] and [self initializeAndStartProcessingGraph]. Let’s discuss the former first (RIOInterface.mm – [line 240]):
- (void)createAUProcessingGraph { OSStatus err; AudioComponentDescription ioUnitDescription; ioUnitDescription.componentType = kAudioUnitType_Output; ioUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO; ioUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple; ioUnitDescription.componentFlags = 0; ioUnitDescription.componentFlagsMask = 0; // Declare and instantiate an audio processing graph NewAUGraph(&processingGraph); // Add an audio unit node to the graph, then instantiate the audio unit. AUNode ioNode; AUGraphAddNode(processingGraph, &ioUnitDescription, &ioNode); AUGraphOpen(processingGraph); // indirectly performs audio unit instantiation // Obtain a reference to the newly-instantiated I/O unit. Each Audio Unit // requires its own configuration. AUGraphNodeInfo(processingGraph, ioNode, NULL, &ioUnit); // Initialize below. AURenderCallbackStruct callbackStruct = {0}; UInt32 enableInput; UInt32 enableOutput; // Enable input and disable output. enableInput = 1; enableOutput = 0; callbackStruct.inputProc = RenderFFTCallback; callbackStruct.inputProcRefCon = self; err = AudioUnitSetProperty(ioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, kInputBus, &enableInput, sizeof(enableInput)); err = AudioUnitSetProperty(ioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, kOutputBus, &enableOutput, sizeof(enableOutput)); err = AudioUnitSetProperty(ioUnit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Input, kOutputBus, &callbackStruct, sizeof(callbackStruct)); // Set the stream format. size_t bytesPerSample =[self ASBDForSoundMode]; err = AudioUnitSetProperty(ioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, kInputBus, &streamFormat, sizeof(streamFormat)); err = AudioUnitSetProperty(ioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, kOutputBus, &streamFormat, sizeof(streamFormat)); // Disable system buffer allocation. We'll do it ourselves. UInt32 flag = 0; err = AudioUnitSetProperty(ioUnit, kAudioUnitProperty_ShouldAllocateBuffer, kAudioUnitScope_Output, kInputBus, &flag, sizeof(flag)); // Allocate AudioBuffers for use when listening. // TODO: Move into initialization...should only be required once. bufferList = (AudioBufferList *)malloc(sizeof(AudioBuffer)); bufferList->mNumberBuffers = 1; bufferList->mBuffers[0].mNumberChannels = 1; bufferList->mBuffers[0].mDataByteSize = 512*bytesPerSample; bufferList->mBuffers[0].mData = calloc(512, bytesPerSample); } |
Some of the comments have been removed for conciseness. Just know this method builds the AUGraph, instantiates the Remote I/O node, and specifies all the different properties needed for input to occur (e.g. enabling input, stream type, sample rate, etc). Anyone who has done work with Core Audio knows setting these properties correctly is half the battle and can cause lots of headache if done incorrectly.
Note the input data grabbed from the microphone is represented by a Singed 16-bit integers in LinearPCM format. Initially I was using the 8.24 fixed-point format specified in the documentation, which lead to many many hours of debugging other parts of my code
Audio Processing
Once we’ve set up all the components, we need to tell them to start! This happens in the second method we mentioned above
(RIOInterface.mm – [line 75])
- (void)initializeAndStartProcessingGraph |
This method calls AUGraphStart(processingGraph) which tells the device to begin sampling audio signals and passing them to the callback function we declared when setting up the AUGraph.
Now it’s time for the good stuff! When the device has a set of audio data to give us, it calls OSStatus RenderFFTCallback(…) (RIOInterface.mm – [line 90]):
OSStatus RenderFFTCallback (void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) { RIOInterface* THIS = (RIOInterface *)inRefCon; COMPLEX_SPLIT A = THIS->A; void *dataBuffer = THIS->dataBuffer; float *outputBuffer = THIS->outputBuffer; FFTSetup fftSetup = THIS->fftSetup; uint32_t log2n = THIS->log2n; uint32_t n = THIS->n; uint32_t nOver2 = THIS->nOver2; uint32_t stride = 1; int bufferCapacity = THIS->bufferCapacity; SInt16 index = THIS->index; AudioUnit rioUnit = THIS->ioUnit; OSStatus renderErr; UInt32 bus1 = 1; renderErr = AudioUnitRender(rioUnit, ioActionFlags, inTimeStamp, bus1, inNumberFrames, THIS->bufferList); if (renderErr < 0) { return renderErr; } // Fill the buffer with our sampled data. If we fill our buffer, run the // fft. int read = bufferCapacity - index; if (read > inNumberFrames) { memcpy((SInt16 *)dataBuffer + index, THIS->bufferList->mBuffers[0].mData, inNumberFrames*sizeof(SInt16)); THIS->index += inNumberFrames; } else { // If we enter this conditional, our buffer will be filled and we should // perform the FFT. memcpy((SInt16 *)dataBuffer + index, THIS->bufferList->mBuffers[0].mData, read*sizeof(SInt16)); // Reset the index. THIS->index = 0; /*************** FFT ***************/ // We want to deal with only floating point values here. ConvertInt16ToFloat(THIS, dataBuffer, outputBuffer, bufferCapacity); /** Look at the real signal as an interleaved complex vector by casting it. Then call the transformation function vDSP_ctoz to get a split complex vector, which for a real signal, divides into an even-odd configuration. */ vDSP_ctoz((COMPLEX*)outputBuffer, 2, &A, 1, nOver2); // Carry out a Forward FFT transform. vDSP_fft_zrip(fftSetup, &A, stride, log2n, FFT_FORWARD); // The output signal is now in a split real form. Use the vDSP_ztoc to get // a split real vector. vDSP_ztoc(&A, 1, (COMPLEX *)outputBuffer, 2, nOver2); // Determine the dominant frequency by taking the magnitude squared and // saving the bin which it resides in. float dominantFrequency = 0; int bin = -1; for (int i=0; i dominantFrequency) { dominantFrequency = curFreq; bin = (i+1)/2; } } memset(outputBuffer, 0, n*sizeof(SInt16)); // Update the UI with our newly acquired frequency value. [THIS->listener frequencyChangedWithValue:bin*(THIS->sampleRate/bufferCapacity)]; printf("Dominant frequency: %f bin: %d n", bin*(THIS->sampleRate/bufferCapacity), bin); } return noErr; } |
In case you didn’t recognize already, this is a C function. The first parameter is a pointer to the Objective-C class that we then cast in order to get references to the class members. After all variables have been set up, we then call:
renderErr = AudioUnitRender(rioUnit, ioActionFlags, inTimeStamp, bus1, inNumberFrames, THIS->bufferList); |
This method “Renders” sampled audio data from the microphone into bufferList. (Recall we allocated bufferList when we set up the AUGraph earlier).
The next if-else statement copies the buffered data just sampled into a larger buffer (dataBuffer). If dataBuffer is not filled, the callback is finished. When the buffer is filled, we perform the FFT.
The first step in performing the FFT is converting our buffered data into the correct format. After a little bit of setup, we can let the CoreAudio API do this for us. You can take a look at the code yourself in void ConvertInt16ToFloat(…).
Once our data is in floating point format, we can use the Accelerate Framework Apple released for iOS 4.0. These method calls have been highly optimized by the engineers at Apple. If you want to learn more, there is a video you can download from WWDC 2010 that discusses performance and comparisons to other libraries (e.g FFTW).
The theory behind the FFT is beyond the scope of this tutorial (nor am I the one who should be explaining it), but you should know that when all is said and done, the sampled data will reside in the frequency domain. All that is left is to apply the algorithm of your choosing to the data to get the dominant frequency. As I said beforehand, the algorithm I use is by no means the best, but it gets the job done.
After we have performed our FFT, we clear the buffer and update the UI to show the current frequency. That’s it!
Hi
Thanks for the example. I have been trying to get my head around RemoteIO for a week or so now and finding it quite difficult. I have created a guitar tuner using AudioQueues which works ok but my response time is around 500ms which is just about quick enough if you continuly pluck a string to get the frequency but I wanted more speed so switched to learning AudioUnits.
I loaded up your project which is working in the simulator but not on the iPhone 3GS.
Also, when testing in the simulator, I pluck the low E string on my guitar (329.6 Hz) and the freq readout is 322 (out of tune) but it doesn’t pick up the change in pitch as a tune the guitar while it is still listening.
Also it is producing a lot of high frequency results when there is silence in the room.
Any ideas?
Thanks
Geoff
Hey thanks for the great example. In response to Geoff, this example uses an FFT with 2056 frames at a sample rate of 22,050hz. This gives your fft a frequency resolution of sampleRate/frames = 10.7hz. For detecting the small frequency changes required for tuning an instrument you need a much more fine resolution, 1hz-2hz or so (however you want it to be). Your 1st string E was within that 10.7hz margin so it could not identify the small changes in pitch. For the simplest solution, I suggest increasing the maxFrames in line 203 of the file RIOInterface.mm (make sure it is a multiple of 2 and dont make it too big or it will crash the program). This method will also give you a slower update rate but thats just the trade off of using ffts. Also, the method of determining the pitch (or fundamental frequency) may not be best suited for guitar tuning as it simply looks at the harmonic with the largest magnitude and calls that the pitch. For guitar, especially the lower wound strings, the harmonic with the most power is not always the fundamental.
Hi
Thanks for the reply. I will give your suggestion a try and see how it reacts. As to the harmonics of a string instrument, I have some code that attempts to fold the harmonics over each other at half their magnitude until the fundamental remains. Its about 50% correct at the lower frequency ranges <85Hz (I can detect a 0.25 difference in Hz) but I get a 97% accuracy of determining the octave greater than 85Hz. I will have to see if I can incorporate this.
Thanks
Anim
@Ryan – Thanks for the explanation!
@ Geoff – I looked into the simulator vs. device issue and if you double the size (from 256->512) in the AudioBuffer allocation, it seems to fix the problem (though I have no idea why). Since the simulator uses native hardware for performing audio processing, I think it may just be coincidental the 256 size worked).
I’ve updated the source code appropriately. I only have an iPad so that’s the device I tested this on. If you are still having issues on the 3GS let me know.
To add on what Ryan said, if you are going to be buffering additional data before performing your fft (which is the proper way to get higher fidelity), make sure you resize your audio buffers accordingly.
As for the high frequency readings in silence, I’m not sure why that is happening. My guess would be whatever noise the microphone is detecting is being analyzed to those high frequencies. If I am incorrect in my assumption, feel free to correct me
Thanks, I will have a play today and see if I can get closer to my goal of 100% accuracy across all strings on the guitar. Its getting there slowly
I will also look at the silence frequency and see whats going on there (that was testing in the simulator on an iMac 24″)
Thanks again
Geoff
Many thanks for putting up this tutorial! I’ve been trying to wrap my head around different available fft libraries and this is by far most clear documentation how to use FFT library provided by iOS. Great job.
I have an issue with running this on iOS 4.3.2 iPod touch 4g. The project builds but ‘Begin Listening’ does not return a value when pressed – current pitch remains n/a. Is this a device issue?
Hey w__,
Sorry for the late reply. Unfortunately, I don’t own that iPod model so I can’t test it locally.
What sort of console output are you getting (if any)?
-D
Hi Demetri
Here’s the console output. I’ve just tried on an iPad 2 on 4.3.2 with similar results. I tried it in the simulator and it returned a frequency initially but only lasted a second or so.
[code]
GNU gdb 6.3.50-20050815 (Apple version gdb-1518) (Sat Feb 12 02:56:02 UTC 2011)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "--host=x86_64-apple-darwin --target=arm-apple-darwin".tty /dev/ttys000
target remote-mobile /tmp/.XcodeGDBRemote-44820-44
Switching to remote-macosx protocol
mem 0x1000 0x3fffffff cache
mem 0x40000000 0xffffffff none
mem 0x00000000 0x0fff none
[Switching to process 11779 thread 0x0]
[Switching to process 11779 thread 0x0]
sharedlibrary apply-load-rules all
warning: Unable to read symbols for /Library/MobileSubstrate/MobileSubstrate.dylib (file not found).
2011-04-23 13:14:50.896 PitchDetector[298:607] MS:Notice: Installing: com.sleepyleaf.safesound [PitchDetector] (550.58)
2011-04-23 13:14:50.939 PitchDetector[298:607] MS:Notice: Loading: /Library/MobileSubstrate/DynamicLibraries/Activator.dylib
warning: Unable to read symbols for /Library/MobileSubstrate/DynamicLibraries/Activator.dylib (file not found).
warning: Unable to read symbols for /Developer/Platforms/iPhoneOS.platform/DeviceSupport/4.3.2 (8H7)/Symbols/System/Library/AccessibilityBundles/AccessibilitySettingsLoader.bundle/AccessibilitySettingsLoader (file not found).
warning: Unable to read symbols for /Developer/Platforms/iPhoneOS.platform/DeviceSupport/4.3.2 (8H7)/Symbols/System/Library/AccessibilityBundles/UIKit.axbundle/UIKit (file not found).
2011-04-23 13:14:52.630 PitchDetector[298:607] Could not load the "Sound_Wave.jpg" image referenced from a nib in the bundle with identifier "com.sleepyleaf.safesound"
2011-04-23 13:14:56.941 PitchDetector[298:607] Sample Rate: 22050
2011-04-23 13:14:56.943 PitchDetector[298:607] Format ID: lpcm
2011-04-23 13:14:56.945 PitchDetector[298:607] Format Flags: C
2011-04-23 13:14:56.946 PitchDetector[298:607] Bytes per Packet: 2
2011-04-23 13:14:56.948 PitchDetector[298:607] Frames per Packet: 1
2011-04-23 13:14:56.949 PitchDetector[298:607] Bytes per Frame: 2
2011-04-23 13:14:56.951 PitchDetector[298:607] Channels per Frame: 1
2011-04-23 13:14:56.955 PitchDetector[298:607] Bits per Channel: 16
[/code]
I’m having the same problem using an iPod touch running 4.2.1. I added some debug breakpoints, and see that RenderFFTCallback is never getting called at all when run on the iPod. Definitely works when run on the simulator, though.
No, sorry…I made a mistake.
RenderFFTCallback is in fact called, but always exits at:
if (renderErr < 0) {
return renderErr;
}
Working Apple brand microphone+earbuds are connected to the iPod during the testing….
Hey guys,
Just so you know, I’m not ignoring you
Unfortunately, this week is fairly hectic with finals at school and I don’t have the time to investigate further. I’ll be sure to take a look at the issues you all have mentioned either this weekend or the beginning of next week.
Thanks!
Demetri
Thats great…we appreciate the help – its great code!
(PS – I noticed that the renderErr I’m getting is -50…..)
Demetri and Joel
I’ve just got this working on my device (ipod touch 4G on iOS 4.3.2. Here’s what I changed, starting from a fresh source tar:
1. ListenerViewController’s header file declared a float ‘currentFrequecy’ … ‘changed it to ‘currentFrequency’
2. RIOInterface’s header set kBufferSize = 512
And the bufferList is now expecting a different size too, so…
3. RIOInterface’s implementation must be changed to the new values here:
bufferList->mBuffers[0].mDataByteSize = 512*bytesPerSample;
bufferList->mBuffers[0].mData = calloc(512, bytesPerSample);
Let me know is this works for you.
Yes! This totally took care of the problem. (I have no idea why, but it did.) Thank you so much for the help!
w__,
Nice work tracking that one down! I had actually encountered this bug a few months ago when I began testing on my personal iPad and fixed it the same way you did. I thought I had pushed the changes to my repository, but I guess not… Hope it didn’t cause you too much grief. Sorry about that :
I have no idea what caused the problem either. I filed a bug report with Apple, but they never got back to me.
I’ve pushed the changes to my repository.
Thanks!
Demetri
Hi Demetri,
I am unable to recognize the lower frequency. I am working with the musical note and in that the lower node like c3,c2 got low frequency and because of the extra noise I am unable to detect these note. The frequency of these notes are bellow 300 Hz.
Please help me find out the way that I can detect these notes.
Hey I have used this algorithm but I am not able to get lower frequency because of noise in environment.
Please guide me..
To cut off the noise environment you need to apply a High-Pass Filter on it. You can find the formula on Wikipedia.
I am trying to figure this out myself as well
HEY I have used this algorithm.
But I m not getting lower frequency like 50hz and 100 hz.
Due to noise in environment I am not able to get the lower frequency so please help me to get the lower frequency
Thanks Demetri.
Great tut! Thanks alot… love fiddling with this code
i have used your sourcecode its giving 10 errrors like CADebugPrintf.h file or directory doesnot exist
DebugPrintFileComma was not declared
DebugPrintfRtn was not declared
In the label always its showing 0.0. or -0.0
Hey Demetri,
Nice tut, thanks for sharing! However, I don’t understand the following line:
`vDSP_ctoz((COMPLEX*)outputBuffer, 2, &A, 1, nOver2);`
You look at the `outputBuffer` as if it would be interleveled, but I don’t think it is. Or I’m missing something?
Hey Radu,
I believe the cast is necessary for converting the real signal rendered from the audio buffer into a split complex vector (which is necessary to perform the FFT). I pulled this line almost directly from the sample code provided by Apple for the vDSP functions. Hopefully a read through that will help?
http://developer.apple.com/library/mac/#documentation/Performance/Conceptual/vDSP_Programming_Guide/SampleCode/SampleCode.html
Take a look at the RealFFTUsageAndTiming() function.
Do you think it would be a straightforward process to modify this so that it could produce pitch analysis of a short pre-recorded sound?
I don’t think it would be too difficult. All you should have to do is replace the system-generated samples with your own and feed them to the analysis code. Do this is a sequential manner and it should just work.
Hope this helps!
Hi Demitri!
I just tried running your current codebase on an iPod4 running iOS5. I did not change any code.
AudioUnitRender() is returning -50.
Here’s the output:
2012-10-18 11:09:14.143 PitchDetector[442:707] Sample Rate: 44100
2012-10-18 11:09:14.147 PitchDetector[442:707] Format ID: lpcm
2012-10-18 11:09:14.152 PitchDetector[442:707] Format Flags: C
2012-10-18 11:09:14.157 PitchDetector[442:707] Bytes per Packet: 2
2012-10-18 11:09:14.161 PitchDetector[442:707] Frames per Packet: 1
2012-10-18 11:09:14.165 PitchDetector[442:707] Bytes per Frame: 2
2012-10-18 11:09:14.169 PitchDetector[442:707] Channels per Frame: 1
2012-10-18 11:09:14.173 PitchDetector[442:707] Bits per Channel: 16
I had to up the buffer (used kBuffer = 1024) to work on iPhone 4s w/ xCode 4.5…else would get err = -50. Not sure what the ACTUAL buffer size on a device should be???
THanks very much for the code.
Current pitch is always n/a.
in RenderFFTCallback() AudioUnitRender() returns -50
i’m using iphone4s ios6
Hey Demetri,
thank you for this great tutorial!!
Unfortunately, I get the same error that has been described previously: I can run it on the simulator, but not on my phone. Your previous suggestions however seem to be integrated in this version. After I press ‘Begin Listening’, the normal output appears, but the label doesn’t change…
2012-12-27 14:51:05.858 PitchDetector[1469:707] Sample Rate: 44100
2012-12-27 14:51:05.860 PitchDetector[1469:707] Format ID: lpcm
2012-12-27 14:51:05.862 PitchDetector[1469:707] Format Flags: C
2012-12-27 14:51:05.863 PitchDetector[1469:707] Bytes per Packet: 2
2012-12-27 14:51:05.864 PitchDetector[1469:707] Frames per Packet: 1
2012-12-27 14:51:05.864 PitchDetector[1469:707] Bytes per Frame: 2
2012-12-27 14:51:05.865 PitchDetector[1469:707] Channels per Frame: 1
2012-12-27 14:51:05.866 PitchDetector[1469:707] Bits per Channel: 16
I’m using the 4s with iOS 5.1… Any thoughts on how I could fix this?
Thank you!!!
Levi
Got it, it was again the buffer size… Set it up to 1024 and it worked.
How come it doesn’t recognize frequencies of 20-22khz though? I saw in a commercial pitch detection tool that the iPhone’s mic should go up to 22khz. Any thoughts on that?
Was testing with http://www.freemosquitoringtones.org/
Thanks Demetri!!
Hello, so thank you for sharing.
I have a question.
1. when there’s a sound lower than 3 octave, recognizing level is going down. this app is confused. it shows C3, C4 repeatedly. Is there a solution?
2. with this code, can I detect a chord? I’m trying to do, but not good.
2.1 can I cut small sounds? or check volume of sounds?
I love your pitch-detector!
Using it for an app that converts frequencies to colors.
I’m trying to find out how to apply the input level as well.
How would I read out the input level together with the frequency?
Glad you like it! I’m not sure what you mean by input level. Could you clarify?