Skip to content

Commit

Permalink
MacOS Standalone Audio Settings Screen; Restartable Audio
Browse files Browse the repository at this point in the history
This commit adds an audio settings screen for the macOS
standalone, but also updates the generic standalone host
to allow audio resets and way more accuracy in setting
and managing things like sample rates and devices.

The default behavior is still to bind to system default,
and this commit doesn't change that, but does make it editable.

To complete this work some things to do include

- Block size selection and error handling
- Save the state of your setup after you edit it in the patch
  and settings file
- Add MIDI device selection just like we have audio
  device selection

But this is an excellent checkpoint into next, so pushing
  • Loading branch information
baconpaul committed Mar 23, 2024
1 parent e789718 commit 28136da
Show file tree
Hide file tree
Showing 7 changed files with 495 additions and 46 deletions.
6 changes: 0 additions & 6 deletions src/detail/standalone/entry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,6 @@ std::shared_ptr<Clap::Plugin> mainCreatePlugin(const clap_plugin_entry *ee, cons
}
}

plugin->setSampleRate(48000);
plugin->setBlockSizes(32, 1024);
plugin->activate();

plugin->start_processing();

return plugin;
}

Expand Down
3 changes: 3 additions & 0 deletions src/detail/standalone/macos/AppDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@

- (IBAction)openAudioSettingsWindow:(id)sender;

- (IBAction)streamWrapperFileAs:(id)sender;
- (IBAction)openWrapperFile:(id)sender;

@end
303 changes: 298 additions & 5 deletions src/detail/standalone/macos/AppDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

#include <AVFoundation/AVFoundation.h>

#include <map>

#include "detail/standalone/entry.h"
#include "detail/standalone/standalone_details.h"
#include "detail/standalone/standalone_host.h"
Expand All @@ -12,6 +14,17 @@ @interface AppDelegate ()

@end

@interface AudioSettingsWindow : NSWindow
{
NSPopUpButton *outputSelection, *inputSelection, *sampleRateSelection;
std::vector<RtAudio::DeviceInfo> outDevices, inDevices;
}

- (void)setupContents;
- (void)resetSampleRateSelection;

@end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
Expand Down Expand Up @@ -116,12 +129,28 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
[[self window] setContentSize:sz];
return false;
};

freeaudio::clap_wrapper::standalone::getStandaloneHost()->displayAudioError = [](auto &s)
{
NSLog(@"Error Reported: %s", s.c_str());
@autoreleasepool
{
NSAlert *alert = [[NSAlert alloc] init];
[alert setMessageText:@"Unable to configure audio"];
[alert setInformativeText:[[NSString alloc] initWithUTF8String:s.c_str()]];
[alert addButtonWithTitle:@"OK"];
[alert runModal];
}
};
freeaudio::clap_wrapper::standalone::mainStartAudio();
}

- (void)applicationWillTerminate:(NSNotification *)aNotification
{
LOG << "applicationWillTerminate shutdown" << std::endl;
freeaudio::clap_wrapper::standalone::getStandaloneHost()->displayAudioError = nullptr;
freeaudio::clap_wrapper::standalone::getStandaloneHost()->onRequestResize = nullptr;

auto plugin = freeaudio::clap_wrapper::standalone::getMainPlugin();

if (plugin && plugin->_ext._gui)
Expand All @@ -130,13 +159,31 @@ - (void)applicationWillTerminate:(NSNotification *)aNotification
plugin->_ext._gui->destroy(plugin->_plugin);
}

// Insert code here to tear down your application
[[self window] setDelegate:nil];
[[self window] release];

freeaudio::clap_wrapper::standalone::mainFinish();
}

- (IBAction)openAudioSettingsWindow:(id)sender
{
NSLog(@"openAudioSettingsWindow: Unimplemented");
@autoreleasepool
{
NSRect windowRect = NSMakeRect(0, 0, 400, 360);

auto *window = [[AudioSettingsWindow alloc]
initWithContentRect:windowRect
styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable
backing:NSBackingStoreBuffered
defer:NO];

[window setupContents];

// Center the window and make it key window and front.
[window center];
[window makeKeyAndOrderFront:nil];
}
}

- (void)windowDidResize:(NSNotification *)notification
Expand Down Expand Up @@ -188,7 +235,7 @@ - (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)frameSize
return frameSize;
}

- (IBAction)saveDocumentAs:(id)sender
- (IBAction)streamWrapperFileAs:(id)sender
{
NSSavePanel *savePanel = [NSSavePanel savePanel];
[savePanel setNameFieldStringValue:@"Untitled"]; //
Expand All @@ -212,12 +259,11 @@ - (IBAction)saveDocumentAs:(id)sender
[alert setInformativeText:[[NSString alloc] initWithUTF8String:e.what()]];
[alert addButtonWithTitle:@"OK"];
[alert runModal];

}
}
}

- (IBAction)openDocument:(id)sender
- (IBAction)openWrapperFile:(id)sender
{
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
[openPanel setCanChooseFiles:YES];
Expand Down Expand Up @@ -249,3 +295,250 @@ - (IBAction)openDocument:(id)sender
}

@end

@implementation AudioSettingsWindow

- (void)setupContents
{
@autoreleasepool
{
auto addLabel = [](NSString *s, int x, int y)
{
NSTextField *label = [[NSTextField alloc] initWithFrame:NSMakeRect(x, y, 200, 30)];

// Set the text of the label
[label setStringValue:s];

// By default, NSTextField objects are editable. Make this one non-editable and non-selectable to act like a label
[label setEditable:NO];
[label setSelectable:NO];
[label setBezeled:NO];
[label setDrawsBackground:NO];

// Add the label to the window
return label;
};
// Set the window title
[self setTitle:@"Audio/MIDI Settings"];

// Create the button
NSButton *okButton = [[NSButton alloc] initWithFrame:NSMakeRect(400 - 80 - 80, 0, 80, 30)];
[okButton setTitle:@"OK"];
[okButton setButtonType:NSMomentaryLightButton]; // Set the button type
[okButton setBezelStyle:NSRoundedBezelStyle];
[okButton setTarget:self];
[okButton setAction:@selector(okButtonPressed:)];

[[self contentView] addSubview:okButton];

NSButton *cancelButton = [[NSButton alloc] initWithFrame:NSMakeRect(400 - 80, 0, 80, 30)];
[cancelButton setTitle:@"Cancel"];
[cancelButton setButtonType:NSMomentaryLightButton]; // Set the button type
[cancelButton setBezelStyle:NSRoundedBezelStyle];
[cancelButton setTarget:self];
[cancelButton setAction:@selector(cancelButtonPressed:)];

[[self contentView] addSubview:addLabel(@"Output", 10, 320)];
outputSelection = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(100, 320, 290, 30)];

[[self contentView] addSubview:addLabel(@"Sample Rate", 10, 285)];
sampleRateSelection = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(100, 285, 120, 30)];

[[self contentView] addSubview:addLabel(@"Input", 10, 250)];
inputSelection = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(100, 250, 290, 30)];

NSButton *defaultButton = [[NSButton alloc] initWithFrame:NSMakeRect(95, 215, 300, 30)];
[defaultButton setTitle:@"Reset to System Default"];
[defaultButton setButtonType:NSMomentaryLightButton]; // Set the button type
[defaultButton setBezelStyle:NSRoundedBezelStyle];
[defaultButton setTarget:self];
[defaultButton setAction:@selector(defaultButtonPressed:)];
[[self contentView] addSubview:defaultButton];

NSBox *horizontalRule = [[NSBox alloc] initWithFrame:NSMakeRect(10, 205, 380, 1)];
[horizontalRule setBoxType:NSBoxSeparator];
[[self contentView] addSubview:horizontalRule];

[[self contentView] addSubview:addLabel(@"MIDI", 10, 165)];
[[self contentView] addSubview:addLabel(@"Coming Soon", 100, 165)];

auto standaloneHost = freeaudio::clap_wrapper::standalone::getStandaloneHost();
outDevices = standaloneHost->getOutputAudioDevices();
// add items to menu
int selIdx{-1}, idx{0};
//[outputSelection addItemWithTitle:@"No Output"];

for (auto o : outDevices)
{
[outputSelection addItemWithTitle:[[NSString alloc] initWithUTF8String:o.name.c_str()]];
if (standaloneHost->audioOutputDeviceID == o.ID) selIdx = idx;
idx++;
}
if (selIdx >= 0)
{
[outputSelection selectItemAtIndex:selIdx];
}
[outputSelection setAction:@selector(onSourceMenuChanged:)];
[outputSelection setTarget:self];

inDevices = standaloneHost->getInputAudioDevices();
selIdx = -1;
idx = 1;
[inputSelection addItemWithTitle:@"No Input"];

for (auto i : inDevices)
{
[inputSelection addItemWithTitle:[[NSString alloc] initWithUTF8String:i.name.c_str()]];
if (standaloneHost->audioInputDeviceID == i.ID) selIdx = idx;
idx++;
}
if (selIdx >= 0)
{
[inputSelection selectItemAtIndex:selIdx];
}

[inputSelection setAction:@selector(onSourceMenuChanged:)];
[inputSelection setTarget:self];

[self resetSampleRateSelection];

// Add the outputSelection to the window's content view
[[self contentView] addSubview:outputSelection];
[[self contentView] addSubview:inputSelection];
[[self contentView] addSubview:sampleRateSelection];

// Add the button to the window's content view
[[self contentView] addSubview:cancelButton];
}
}

- (void)okButtonPressed:(id)sender
{
@autoreleasepool
{
unsigned int outId{0}, inId{0};
bool useOut{false}, useIn{false};

auto oidx = [outputSelection indexOfSelectedItem];
if (oidx >= 0) // modify this to > and add a -1 below if we add no out
{
const auto &oDev = outDevices[oidx];
outId = oDev.ID;
useOut = true;
}

auto iidx = [inputSelection indexOfSelectedItem];
if (iidx > 0)
{
const auto &iDev = inDevices[iidx - 1];
inId = iDev.ID;
useIn = true;
}

const auto sr = [[[sampleRateSelection selectedItem] title] integerValue];

auto standaloneHost = freeaudio::clap_wrapper::standalone::getStandaloneHost();
standaloneHost->startAudioThreadOn(inId, 2, useIn, outId, 2, useOut, sr);

[self close];
}
}

- (void)defaultButtonPressed:(id)sender
{
auto standaloneHost = freeaudio::clap_wrapper::standalone::getStandaloneHost();
auto [in, out, sr] = standaloneHost->getDefaultAudioInOutSampleRate();
int idx = 1;
for (auto i : inDevices)
{
if ((int)i.ID == (int)in)
{
[inputSelection selectItemAtIndex:idx];
}
idx++;
}

idx = 0;
for (auto o : outDevices)
{
if ((int)o.ID == (int)out)
{
[outputSelection selectItemAtIndex:idx];
}
idx++;
}

[self resetSampleRateSelection];

for (NSMenuItem *item in [sampleRateSelection itemArray])
{
const auto sri = [[item title] integerValue];
if ((int)sr == (int)sri)
{
[sampleRateSelection selectItem: item];
}

}
}

- (void)cancelButtonPressed:(id)sender
{
@autoreleasepool
{
[self close];
}
}

- (void)onSourceMenuChanged:(id)sender
{
[self resetSampleRateSelection];
}

- (void)resetSampleRateSelection
{
auto idx = [outputSelection indexOfSelectedItem];
const auto &oDev = outDevices[idx];

[sampleRateSelection removeAllItems];
auto csr = freeaudio::clap_wrapper::standalone::getStandaloneHost()->currentSampleRate;

std::map<int, int> srAvail;
for (auto sr : oDev.sampleRates)
{
srAvail[sr]++;
}

idx = [inputSelection indexOfSelectedItem];
if (idx > 0)
{
const auto &iDev = inDevices[idx - 1];

for (auto sr : iDev.sampleRates)
{
srAvail[sr]++;
}
}
else
{
// Just take the output rates
for (auto sr : oDev.sampleRates)
{
srAvail[sr]++;
}
}

int selIdx{-1}, sIdx{0};
for (auto [sr, ct] : srAvail)
{
if (ct == 2)
{
[sampleRateSelection addItemWithTitle:[NSString stringWithFormat:@"%d", sr]];
if ((int)sr == (int)csr) selIdx = sIdx;

sIdx++;
}
}
if (selIdx >= 0) [sampleRateSelection selectItemAtIndex:selIdx];
}

@end
Loading

0 comments on commit 28136da

Please sign in to comment.