Headphone Virtualization: Implement customization

Implement the ability to configure and select an HRIR preset to use with
the HRIR filter, or remove the preset. It will validate the file's
usefulness before setting it for the player to use.

Also, fixed back center channel filtering for 7.0 format audio.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
CQTexperiment
Christopher Snowhill 2022-01-25 21:30:33 -08:00
parent bb029757fd
commit 708c7dc721
7 changed files with 283 additions and 83 deletions

View File

@ -88,6 +88,7 @@ void PrintStreamDesc (AudioStreamBasicDescription *inDesc)
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.volumeScaling" options:0 context:nil];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.outputResampling" options:0 context:nil];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.headphoneVirtualization" options:0 context:nil];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.hrirPath" options:0 context:nil];
}
return self;
@ -1109,7 +1110,8 @@ tryagain:
[self inputFormatDidChange:inputFormat];
}
}
else if ([keyPath isEqualToString:@"values.headphoneVirtualization"]) {
else if ([keyPath isEqualToString:@"values.headphoneVirtualization"] ||
[keyPath isEqualToString:@"values.hrirPath"]) {
// Reset the converter, without rebuffering
if (outputFormat.mChannelsPerFrame == 2 &&
inputFormat.mChannelsPerFrame >= 1 &&
@ -1231,10 +1233,27 @@ static float db_to_scale(float db)
outputFormat.mChannelsPerFrame == 2 &&
inputFormat.mChannelsPerFrame >= 1 &&
inputFormat.mChannelsPerFrame <= 8) {
CFURLRef appUrlRef = CFBundleCopyResourceURL(CFBundleGetMainBundle(), CFSTR("gsx"), CFSTR("wv"), NULL);
NSString * userPreset = [[[NSUserDefaultsController sharedUserDefaultsController] defaults] stringForKey:@"hrirPath"];
NSURL * presetUrl = nil;
if (userPreset && ![userPreset isEqualToString:@""]) {
presetUrl = [NSURL fileURLWithPath:userPreset];
if (![HeadphoneFilter validateImpulseFile:presetUrl])
presetUrl = nil;
}
if (!presetUrl) {
CFURLRef appUrlRef = CFBundleCopyResourceURL(CFBundleGetMainBundle(), CFSTR("gsx"), CFSTR("wv"), NULL);
if (appUrlRef)
presetUrl = (__bridge NSURL *) appUrlRef;
CFRelease(appUrlRef);
if (![HeadphoneFilter validateImpulseFile:presetUrl])
presetUrl = nil;
}
if (appUrlRef) {
hFilter = [[HeadphoneFilter alloc] initWithImpulseFile:(__bridge NSURL *)appUrlRef forSampleRate:outputFormat.mSampleRate withInputChannels:inputFormat.mChannelsPerFrame];
if (presetUrl) {
hFilter = [[HeadphoneFilter alloc] initWithImpulseFile:presetUrl forSampleRate:outputFormat.mSampleRate withInputChannels:inputFormat.mChannelsPerFrame];
}
}
@ -1305,7 +1324,8 @@ static float db_to_scale(float db)
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.volumeScaling"];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.outputResampling"];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.headphoneVirtualization"];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.hrirPath"];
paused = NO;
[self cleanUp];
}

View File

@ -41,6 +41,8 @@
int prevOverlapLength;
}
+ (BOOL)validateImpulseFile:(NSURL *)url;
- (id)initWithImpulseFile:(NSURL *)url forSampleRate:(double)sampleRate withInputChannels:(size_t)channels;
- (void)process:(const float*)inBuffer sampleCount:(size_t)count toBuffer:(float *)outBuffer;

View File

@ -14,6 +14,62 @@
@implementation HeadphoneFilter
// Symmetrical / no-echo sets
static const int8_t speakers_to_hesuvi_7[8][2][8] = {
{ { 6 }, { 6 } }, // mono/center
{ { 0, 1 }, { 1, 0 } }, // left/right
{ { 0, 1, 6 }, { 1, 0, 6 } }, // left/right/center
{ { 0, 1, 4, 5 }, { 1, 0, 5, 4 } },// left/right/left back/right back
{ { 0, 1, 6, 4, 5 }, { 1, 0, 6, 5, 4 } }, // left/right/center/back left/back right
{ { 0, 1, 6, 6, 4, 5 }, { 1, 0, 6, 6, 5, 4 } }, // left/right/center/lfe(center)/back left/back right
{ { 0, 1, 6, 6, -1, 2, 3 }, { 1, 0, 6, 6, -1, 3, 2 } }, // left/right/center/lfe(center)/back center(special)/side left/side right
{ { 0, 1, 6, 6, 4, 5, 2, 3 }, { 1, 0, 6, 6, 5, 4, 3, 2 } } // left/right/center/lfe(center)/back left/back right/side left/side right
};
// Asymmetrical / echo sets
static const int8_t speakers_to_hesuvi_14[8][2][8] = {
{ { 6 }, { 13 } }, // mono/center
{ { 0, 8 }, { 1, 7 } }, // left/right
{ { 0, 8, 6 }, { 1, 7, 13 } }, // left/right/center
{ { 0, 8, 4, 12 }, { 1, 7, 5, 11 } },// left/right/left back/right back
{ { 0, 8, 6, 4, 12 }, { 1, 7, 13, 5, 11 } }, // left/right/center/back left/back right
{ { 0, 8, 6, 6, 4, 12 }, { 1, 7, 13, 13, 5, 11 } }, // left/right/center/lfe(center)/back left/back right
{ { 0, 8, 6, 6, -1, 2, 10 }, { 1, 7, 13, 13, -1, 3, 9 } }, // left/right/center/lfe(center)/back center(special)/side left/side right
{ { 0, 8, 6, 6, 4, 12, 2, 10 }, { 1, 7, 13, 13, 5, 11, 3, 9 } } // left/right/center/lfe(center)/back left/back right/side left/side right
};
+ (BOOL)validateImpulseFile:(NSURL *)url {
id<CogSource> source = [AudioSource audioSourceForURL:url];
if (!source)
return NO;
if (![source open:url])
return NO;
id<CogDecoder> decoder = [AudioDecoder audioDecoderForSource:source];
if (decoder == nil)
return NO;
if (![decoder open:source])
{
return NO;
}
NSDictionary *properties = [decoder properties];
int impulseChannels = [[properties objectForKey:@"channels"] intValue];
if ([[properties objectForKey:@"floatingPoint"] boolValue] != YES ||
[[properties objectForKey:@"bitsPerSample"] intValue] != 32 ||
!([[properties objectForKey:@"endian"] isEqualToString:@"native"] ||
[[properties objectForKey:@"endian"] isEqualToString:@"little"]) ||
(impulseChannels != 14 && impulseChannels != 7))
return NO;
return YES;
}
- (id)initWithImpulseFile:(NSURL *)url forSampleRate:(double)sampleRate withInputChannels:(size_t)channels {
self = [super init];
@ -48,30 +104,35 @@
[[properties objectForKey:@"endian"] isEqualToString:@"little"]) ||
(impulseChannels != 14 && impulseChannels != 7))
return nil;
float * impulseBuffer = calloc(sizeof(float), (sampleCount + 1024) * sizeof(float) * impulseChannels);
[decoder readAudio:impulseBuffer frames:sampleCount];
if (!impulseBuffer)
return nil;
if ([decoder readAudio:impulseBuffer frames:sampleCount] != sampleCount)
return nil;
[decoder close];
decoder = nil;
source = nil;
if (sampleRateOfSource != sampleRate) {
double sampleRatio = sampleRate / sampleRateOfSource;
int resampledCount = (int)ceil((double)sampleCount * sampleRatio);
void *resampler_data = NULL;
const retro_resampler_t *resampler = NULL;
if (!retro_resampler_realloc(&resampler_data, &resampler, "sinc", RESAMPLER_QUALITY_NORMAL, impulseChannels, sampleRatio)) {
free(impulseBuffer);
return nil;
}
int resamplerLatencyIn = (int) resampler->latency(resampler_data);
int resamplerLatencyOut = (int)ceil(resamplerLatencyIn * sampleRatio);
float * resampledImpulse = calloc(sizeof(float), (resampledCount + resamplerLatencyOut * 2 + 128) * sizeof(float) * impulseChannels);
memmove(impulseBuffer + resamplerLatencyIn * impulseChannels, impulseBuffer, sampleCount * sizeof(float) * impulseChannels);
memset(impulseBuffer, 0, resamplerLatencyIn * sizeof(float) * impulseChannels);
memset(impulseBuffer + (resamplerLatencyIn + sampleCount) * impulseChannels, 0, resamplerLatencyIn * sizeof(float) * impulseChannels);
@ -82,34 +143,37 @@
src_data.data_out = resampledImpulse;
src_data.output_frames = 0;
src_data.ratio = sampleRatio;
resampler->process(resampler_data, &src_data);
resampler->free(resampler, resampler_data);
src_data.output_frames -= resamplerLatencyOut * 2;
memmove(resampledImpulse, resampledImpulse + resamplerLatencyOut * impulseChannels, src_data.output_frames * sizeof(float) * impulseChannels);
free(impulseBuffer);
impulseBuffer = resampledImpulse;
sampleCount = (int) src_data.output_frames;
}
channelCount = channels;
bufferSize = 512;
fftSize = sampleCount + bufferSize;
int pow = 1;
while (fftSize > 2) { pow++; fftSize /= 2; }
fftSize = 2 << pow;
float * deinterleavedImpulseBuffer = (float *) memalign_calloc(16, sizeof(float), fftSize * impulseChannels);
if (!deinterleavedImpulseBuffer) {
free(impulseBuffer);
return nil;
}
float * deinterleavedImpulseBuffer = (float *) memalign_calloc(128, sizeof(float), fftSize * impulseChannels);
for (size_t i = 0; i < impulseChannels; ++i) {
for (size_t j = 0; j < sampleCount; ++j) {
deinterleavedImpulseBuffer[i * fftSize + j] = impulseBuffer[i + impulseChannels * j];
}
cblas_scopy(sampleCount, impulseBuffer + i, impulseChannels, deinterleavedImpulseBuffer + i * fftSize, 1);
}
free(impulseBuffer);
@ -120,48 +184,57 @@
log2nhalf = log2n / 2;
fftSetup = vDSP_create_fftsetup(log2n, FFT_RADIX2);
if (!fftSetup) {
memalign_free(deinterleavedImpulseBuffer);
return nil;
}
paddedSignal = (float *) memalign_calloc(128, sizeof(float), paddedBufferSize);
paddedSignal = (float *) memalign_calloc(16, sizeof(float), paddedBufferSize);
if (!paddedSignal) {
memalign_free(deinterleavedImpulseBuffer);
return nil;
}
signal_fft.realp = (float *) memalign_calloc(128, sizeof(float), fftSizeOver2);
signal_fft.imagp = (float *) memalign_calloc(128, sizeof(float), fftSizeOver2);
signal_fft.realp = (float *) memalign_calloc(16, sizeof(float), fftSizeOver2);
signal_fft.imagp = (float *) memalign_calloc(16, sizeof(float), fftSizeOver2);
if (!signal_fft.realp || !signal_fft.imagp) {
memalign_free(deinterleavedImpulseBuffer);
return nil;
}
input_filtered_signal_per_channel[0].realp = (float *) memalign_calloc(128, sizeof(float), fftSizeOver2);
input_filtered_signal_per_channel[0].imagp = (float *) memalign_calloc(128, sizeof(float), fftSizeOver2);
input_filtered_signal_per_channel[1].realp = (float *) memalign_calloc(128, sizeof(float), fftSizeOver2);
input_filtered_signal_per_channel[1].imagp = (float *) memalign_calloc(128, sizeof(float), fftSizeOver2);
input_filtered_signal_per_channel[0].realp = (float *) memalign_calloc(16, sizeof(float), fftSizeOver2);
input_filtered_signal_per_channel[0].imagp = (float *) memalign_calloc(16, sizeof(float), fftSizeOver2);
if (!input_filtered_signal_per_channel[0].realp ||
!input_filtered_signal_per_channel[0].imagp) {
memalign_free(deinterleavedImpulseBuffer);
return nil;
}
input_filtered_signal_per_channel[1].realp = (float *) memalign_calloc(16, sizeof(float), fftSizeOver2);
input_filtered_signal_per_channel[1].imagp = (float *) memalign_calloc(16, sizeof(float), fftSizeOver2);
if (!input_filtered_signal_per_channel[1].realp ||
!input_filtered_signal_per_channel[1].imagp) {
memalign_free(deinterleavedImpulseBuffer);
return nil;
}
impulse_responses = (COMPLEX_SPLIT *) calloc(sizeof(COMPLEX_SPLIT), channels * 2);
if (!impulse_responses) {
memalign_free(deinterleavedImpulseBuffer);
return nil;
}
// Symmetrical / no-echo sets
const int8_t speakers_to_hesuvi_7[8][2][8] = {
{ { 6, }, { 6, } }, // mono/center
{ { 0, 1 }, { 1, 0 } }, // left/right
{ { 0, 1, 6 }, { 1, 0, 6 } }, // left/right/center
{ { 0, 1, 4, 5 }, { 1, 0, 5, 4 } },// left/right/left back/right back
{ { 0, 1, 6, 4, 5 }, { 1, 0, 6, 5, 4 } }, // left/right/center/back left/back right
{ { 0, 1, 6, 6, 4, 5 }, { 1, 0, 6, 6, 5, 4 } }, // left/right/center/lfe(center)/back left/back right
{ { 0, 1, 6, 6, -1, 2, 3 }, { 1, 0, 6, 6, -1, 3, 2 } }, // left/right/center/lfe(center)/back center(special)/side left/side right
{ { 0, 1, 6, 6, 4, 5, 2, 3 }, { 1, 0, 6, 6, 5, 4, 3, 2 } } // left/right/center/lfe(center)/back left/back right/side left/side right
};
// Asymmetrical / echo sets
const int8_t speakers_to_hesuvi_14[8][2][8] = {
{ { 6, }, { 13, } }, // mono/center
{ { 0, 8 }, { 1, 7 } }, // left/right
{ { 0, 8, 6 }, { 1, 7, 13 } }, // left/right/center
{ { 0, 8, 4, 12 }, { 1, 7, 5, 11 } },// left/right/left back/right back
{ { 0, 8, 6, 4, 12 }, { 1, 7, 13, 5, 11 } }, // left/right/center/back left/back right
{ { 0, 8, 6, 6, 4, 12 }, { 1, 7, 13, 13, 5, 11 } }, // left/right/center/lfe(center)/back left/back right
{ { 0, 8, 6, 6, -1, 2, 10 }, { 1, 7, 13, 13, -1, 3, 9 } }, // left/right/center/lfe(center)/back center(special)/side left/side right
{ { 0, 8, 6, 6, 4, 12, 2, 10 }, { 1, 7, 13, 13, 5, 11, 3, 9 } } // left/right/center/lfe(center)/back left/back right/side left/side right
};
for (size_t i = 0; i < channels; ++i) {
impulse_responses[i * 2 + 0].realp = (float *) memalign_calloc(128, sizeof(float), fftSizeOver2);
impulse_responses[i * 2 + 0].imagp = (float *) memalign_calloc(128, sizeof(float), fftSizeOver2);
impulse_responses[i * 2 + 1].realp = (float *) memalign_calloc(128, sizeof(float), fftSizeOver2);
impulse_responses[i * 2 + 1].imagp = (float *) memalign_calloc(128, sizeof(float), fftSizeOver2);
impulse_responses[i * 2 + 0].realp = (float *) memalign_calloc(16, sizeof(float), fftSizeOver2);
impulse_responses[i * 2 + 0].imagp = (float *) memalign_calloc(16, sizeof(float), fftSizeOver2);
impulse_responses[i * 2 + 1].realp = (float *) memalign_calloc(16, sizeof(float), fftSizeOver2);
impulse_responses[i * 2 + 1].imagp = (float *) memalign_calloc(16, sizeof(float), fftSizeOver2);
if (!impulse_responses[i * 2 + 0].realp || !impulse_responses[i * 2 + 0].imagp ||
!impulse_responses[i * 2 + 1].realp || !impulse_responses[i * 2 + 1].imagp) {
memalign_free(deinterleavedImpulseBuffer);
return nil;
}
int leftInChannel;
int rightInChannel;
@ -176,22 +249,37 @@
}
if (leftInChannel == -1 || rightInChannel == -1) {
float * temp = calloc(sizeof(float), fftSize * 2);
float * temp;
if (impulseChannels == 7) {
for (size_t i = 0; i < fftSize; i++) {
temp[i + fftSize] = temp[i] = deinterleavedImpulseBuffer[i + 2 * fftSize] + deinterleavedImpulseBuffer[i + 3 * fftSize];
temp = calloc(sizeof(float), fftSize);
if (!temp) {
memalign_free(deinterleavedImpulseBuffer);
return nil;
}
cblas_scopy((int)fftSize, temp, 1, deinterleavedImpulseBuffer + 4 * fftSize, 1);
vDSP_vadd(temp, 1, deinterleavedImpulseBuffer + 5 * fftSize, 1, temp, 1, fftSize);
vDSP_ctoz((DSPComplex *)temp, 2, &impulse_responses[i * 2 + 0], 1, fftSizeOver2);
vDSP_ctoz((DSPComplex *)temp, 2, &impulse_responses[i * 2 + 1], 1, fftSizeOver2);
}
else {
for (size_t i = 0; i < fftSize; i++) {
temp[i] = deinterleavedImpulseBuffer[i + 2 * fftSize] + deinterleavedImpulseBuffer[i + 9 * fftSize];
temp[i + fftSize] = deinterleavedImpulseBuffer[i + 3 * fftSize] + deinterleavedImpulseBuffer[i + 10 * fftSize];
temp = calloc(sizeof(float), fftSize * 2);
if (!temp) {
memalign_free(deinterleavedImpulseBuffer);
return nil;
}
cblas_scopy((int)fftSize, temp, 1, deinterleavedImpulseBuffer + 4 * fftSize, 1);
vDSP_vadd(temp, 1, deinterleavedImpulseBuffer + 12 * fftSize, 1, temp, 1, fftSize);
cblas_scopy((int)fftSize, temp + fftSize, 1, deinterleavedImpulseBuffer + 5 * fftSize, 1);
vDSP_vadd(temp + fftSize, 1, deinterleavedImpulseBuffer + 11 * fftSize, 1, temp + fftSize, 1, fftSize);
vDSP_ctoz((DSPComplex *)temp, 2, &impulse_responses[i * 2 + 0], 1, fftSizeOver2);
vDSP_ctoz((DSPComplex *)(temp + fftSize), 2, &impulse_responses[i * 2 + 1], 1, fftSizeOver2);
}
vDSP_ctoz((DSPComplex *)temp, 2, &impulse_responses[i * 2 + 0], 1, fftSizeOver2);
vDSP_ctoz((DSPComplex *)(temp + fftSize), 2, &impulse_responses[i * 2 + 1], 1, fftSizeOver2);
free(temp);
}
else {
@ -205,14 +293,20 @@
memalign_free(deinterleavedImpulseBuffer);
left_result = (float *) memalign_calloc(128, sizeof(float), fftSize);
right_result = (float *) memalign_calloc(128, sizeof(float), fftSize);
left_result = (float *) memalign_calloc(16, sizeof(float), fftSize);
right_result = (float *) memalign_calloc(16, sizeof(float), fftSize);
if (!left_result || !right_result)
return nil;
prevOverlap[0] = (float *) memalign_calloc(128, sizeof(float), fftSize);
prevOverlap[1] = (float *) memalign_calloc(128, sizeof(float), fftSize);
prevOverlap[0] = (float *) memalign_calloc(16, sizeof(float), fftSize);
prevOverlap[1] = (float *) memalign_calloc(16, sizeof(float), fftSize);
if (!prevOverlap[0] || !prevOverlap[1])
return nil;
left_mix_result = (float *) memalign_calloc(128, sizeof(float), fftSize);
right_mix_result = (float *) memalign_calloc(128, sizeof(float), fftSize);
left_mix_result = (float *) memalign_calloc(16, sizeof(float), fftSize);
right_mix_result = (float *) memalign_calloc(16, sizeof(float), fftSize);
if (!left_mix_result || !right_mix_result)
return nil;
prevOverlapLength = 0;
}

View File

@ -245,7 +245,7 @@
</connections>
</customObject>
<customView id="58" userLabel="OutputView">
<rect key="frame" x="0.0" y="0.0" width="530" height="150"/>
<rect key="frame" x="0.0" y="0.0" width="530" height="185"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8GG-SU-ZrX">
@ -339,7 +339,7 @@
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DhK-tx-xFv">
<rect key="frame" x="18" y="121" width="227" height="18"/>
<rect key="frame" x="18" y="156" width="227" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" title="Limit volume control to 100%" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="ds2-aw-ebU">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
@ -349,19 +349,67 @@
<binding destination="52" name="value" keyPath="values.volumeLimit" id="7Sl-LJ-ljd"/>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="bQz-MX-59V">
<rect key="frame" x="251" y="121" width="227" height="18"/>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="1aX-Fo-arv">
<rect key="frame" x="18" y="119" width="20" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" title="Enable headphone virtualization" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="W0w-wC-1ug">
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="EJb-yF-qRd">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="52" name="value" keyPath="values.headphoneVirtualization" id="wwT-OG-ulF"/>
<binding destination="52" name="value" keyPath="values.headphoneVirtualization" id="1Mx-dJ-ySQ"/>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="4qt-T1-7jG" userLabel="Push Button - Select a SoundFont">
<rect key="frame" x="36" y="111" width="73" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Select" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Otm-xP-EEU" userLabel="Button Cell - Select a SoundFont">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="setHrir:" target="57" id="3Rw-rb-bBp"/>
</connections>
</button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kOF-Po-oeH" userLabel="Push Button - Select a SoundFont">
<rect key="frame" x="102" y="111" width="43" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="X" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="eOk-lh-q1L" userLabel="Button Cell - Select a SoundFont">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="clearHrir:" target="57" id="tLQ-zH-90Z"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="iZm-aN-M1J" userLabel="Text Field - Selected:">
<rect key="frame" x="145" y="120" width="45" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="HRIR:" id="1Bl-uL-Xda" userLabel="Text Field Cell - Selected">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="bsA-1J-aVt">
<rect key="frame" x="187" y="120" width="275" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Label" id="5vB-dF-Jvk">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
<connections>
<binding destination="52" name="value" keyPath="values.hrirPath" id="vtm-Ze-9v6">
<dictionary key="options">
<string key="NSNullPlaceholder">Built-in</string>
<string key="NSValueTransformerName">PathToFileTransformer</string>
</dictionary>
</binding>
</connections>
</textFieldCell>
</textField>
</subviews>
<point key="canvasLocation" x="-151" y="318"/>
<point key="canvasLocation" x="-151" y="317.5"/>
</customView>
<customObject id="i5B-ga-Atm" userLabel="MIDIPane" customClass="MIDIPane">
<connections>

View File

@ -15,5 +15,7 @@
}
- (IBAction) takeDeviceID:(id)sender;
- (IBAction) setHrir:(id)sender;
- (IBAction) clearHrir:(id)sender;
@end

View File

@ -7,7 +7,7 @@
//
#import "OutputPane.h"
#import "HeadphoneFilter.h"
@implementation OutputPane
@ -31,5 +31,37 @@
[[NSUserDefaults standardUserDefaults] setObject: device forKey:@"outputDevice"];
}
- (IBAction)setHrir:(id)sender
{
NSArray *fileTypes = @[@"wav", @"wv"];
NSOpenPanel * panel = [NSOpenPanel openPanel];
[panel setAllowsMultipleSelection:NO];
[panel setCanChooseDirectories:NO];
[panel setCanChooseFiles:YES];
[panel setFloatingPanel:YES];
[panel setAllowedFileTypes:fileTypes];
NSString * oldPath = [[NSUserDefaults standardUserDefaults] stringForKey:@"hrirPath"];
if ( oldPath != nil )
[panel setDirectoryURL:[NSURL fileURLWithPath:oldPath]];
NSInteger result = [panel runModal];
if(result == NSModalResponseOK)
{
NSString * path = [[panel URL] path];
if ([NSClassFromString(@"HeadphoneFilter") validateImpulseFile:[NSURL fileURLWithPath:path]])
[[NSUserDefaults standardUserDefaults] setValue:[[panel URL] path] forKey:@"hrirPath"];
else {
NSAlert *alert = [[NSAlert alloc] init];
[alert setMessageText:@"Invalid impulse"];
[alert setInformativeText:@"The selected file does not conform to the HeSuVi HRIR specification."];
[alert addButtonWithTitle:@"Ok"];
[alert runModal];
}
}
}
- (IBAction)clearHrir:(id)sender
{
[[NSUserDefaults standardUserDefaults] setValue:@"" forKey:@"hrirPath"];
}
@end

View File

@ -115,6 +115,7 @@
83B06728180D85B8008E3612 /* MIDIPane.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MIDIPane.m; sourceTree = "<group>"; };
83B0672A180D8B39008E3612 /* midi.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = midi.png; path = Icons/midi.png; sourceTree = "<group>"; };
83BC5AB320E4C90F00631CD4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/Preferences.xib; sourceTree = "<group>"; };
83D34B7D27A10FA700784D34 /* HeadphoneFilter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HeadphoneFilter.h; path = ../../Audio/Chain/HeadphoneFilter.h; sourceTree = "<group>"; };
83EF495D17FBC96A00642E3C /* VolumeBehaviorArrayController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VolumeBehaviorArrayController.h; sourceTree = "<group>"; };
83EF495E17FBC96A00642E3C /* VolumeBehaviorArrayController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VolumeBehaviorArrayController.m; sourceTree = "<group>"; };
83F27E651810DD3A00CEF538 /* appearance@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "appearance@2x.png"; path = "Icons/appearance@2x.png"; sourceTree = "<group>"; };
@ -195,6 +196,7 @@
children = (
83F27E711810E41A00CEF538 /* Transformers */,
8384913618081ECB00E7332D /* Logging.h */,
83D34B7D27A10FA700784D34 /* HeadphoneFilter.h */,
17D503410ABDB1660022D1E8 /* Custom */,
17D5033F0ABDB1570022D1E8 /* Panes */,
17D1B3F60F6349CE00694C57 /* PreferencePanePlugin.h */,