diff --git a/src/detail/standalone/entry.cpp b/src/detail/standalone/entry.cpp index 6ee9c965..7e3c85f1 100644 --- a/src/detail/standalone/entry.cpp +++ b/src/detail/standalone/entry.cpp @@ -70,12 +70,6 @@ std::shared_ptr mainCreatePlugin(const clap_plugin_entry *ee, cons } } - plugin->setSampleRate(48000); - plugin->setBlockSizes(32, 1024); - plugin->activate(); - - plugin->start_processing(); - return plugin; } diff --git a/src/detail/standalone/macos/AppDelegate.h b/src/detail/standalone/macos/AppDelegate.h index 62361a91..c27c4127 100644 --- a/src/detail/standalone/macos/AppDelegate.h +++ b/src/detail/standalone/macos/AppDelegate.h @@ -10,4 +10,7 @@ - (IBAction)openAudioSettingsWindow:(id)sender; +- (IBAction)streamWrapperFileAs:(id)sender; +- (IBAction)openWrapperFile:(id)sender; + @end diff --git a/src/detail/standalone/macos/AppDelegate.mm b/src/detail/standalone/macos/AppDelegate.mm index 4a6ba574..4b66f522 100644 --- a/src/detail/standalone/macos/AppDelegate.mm +++ b/src/detail/standalone/macos/AppDelegate.mm @@ -2,6 +2,8 @@ #include +#include + #include "detail/standalone/entry.h" #include "detail/standalone/standalone_details.h" #include "detail/standalone/standalone_host.h" @@ -12,6 +14,17 @@ @interface AppDelegate () @end +@interface AudioSettingsWindow : NSWindow +{ + NSPopUpButton *outputSelection, *inputSelection, *sampleRateSelection; + std::vector outDevices, inDevices; +} + +- (void)setupContents; +- (void)resetSampleRateSelection; + +@end + @implementation AppDelegate - (void)applicationDidFinishLaunching:(NSNotification *)aNotification @@ -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) @@ -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 @@ -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"]; // @@ -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]; @@ -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 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 \ No newline at end of file diff --git a/src/detail/standalone/macos/MainMenu.xib b/src/detail/standalone/macos/MainMenu.xib index 50a479ea..2e0b9478 100644 --- a/src/detail/standalone/macos/MainMenu.xib +++ b/src/detail/standalone/macos/MainMenu.xib @@ -69,20 +69,12 @@ - + - - + diff --git a/src/detail/standalone/standalone_host.cpp b/src/detail/standalone/standalone_host.cpp index ee40fed1..03add15b 100644 --- a/src/detail/standalone/standalone_host.cpp +++ b/src/detail/standalone/standalone_host.cpp @@ -364,4 +364,24 @@ bool StandaloneHost::tryLoadStandaloneAndPluginSettings(const fs::path &fromDir, return true; } +void StandaloneHost::activatePlugin(int32_t sr, int32_t minBlock, int32_t maxBlock) +{ + if (isActive) + { + clapPlugin->stop_processing(); + clapPlugin->deactivate(); + isActive = false; + } + + LOG << "Activating plugin : sampleRate=" << sr << " blockBounds=" << minBlock << " to " << maxBlock + << std::endl; + clapPlugin->setSampleRate(sr); + clapPlugin->setBlockSizes(minBlock, maxBlock); + clapPlugin->activate(); + + clapPlugin->start_processing(); + + isActive = true; +} + } // namespace freeaudio::clap_wrapper::standalone diff --git a/src/detail/standalone/standalone_host.h b/src/detail/standalone/standalone_host.h index 0288c8ca..86beebb5 100644 --- a/src/detail/standalone/standalone_host.h +++ b/src/detail/standalone/standalone_host.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "standalone_details.h" @@ -232,11 +233,26 @@ struct StandaloneHost : Clap::IHost // in standalone_host.cpp void clapProcess(void *pOutput, const void *pInoput, uint32_t frameCount); - // In standalone_host_audio.cpp + // Actual audi IO In standalone_host_audio.cpp std::unique_ptr rtaDac; + std::function displayAudioError{nullptr}; + unsigned int audioInputDeviceID{0}, audioOutputDeviceID{0}; + bool audioInputUsed{true}, audioOutputUsed{true}; + int32_t currentSampleRate{0}; + void guaranteeRtAudioDAC(); + std::tuple getDefaultAudioInOutSampleRate(); void startAudioThread(); + void startAudioThreadOn(unsigned int inputDeviceID, uint32_t inputChannels, bool useInput, + unsigned int outputDeviceID, uint32_t outputChannels, bool useOutput, + int32_t sampleRate); void stopAudioThread(); + void activatePlugin(int32_t sr, int32_t minBlock, int32_t maxBlock); + bool isActive{false}; + + std::vector getInputAudioDevices(); + std::vector getOutputAudioDevices(); + clap_input_events inputEvents{}; clap_output_events outputEvents{}; diff --git a/src/detail/standalone/standalone_host_audio.cpp b/src/detail/standalone/standalone_host_audio.cpp index 85de547c..199d590a 100644 --- a/src/detail/standalone/standalone_host_audio.cpp +++ b/src/detail/standalone/standalone_host_audio.cpp @@ -14,6 +14,7 @@ #endif #include "standalone_host.h" +#include "entry.h" namespace freeaudio::clap_wrapper::standalone { @@ -32,60 +33,187 @@ int rtaCallback(void *outputBuffer, void *inputBuffer, unsigned int nBufferFrame void rtaErrorCallback(RtAudioErrorType, const std::string &errorText) { LOG << "[ERROR] RtAudio reports '" << errorText << "'" << std::endl; + auto ae = getStandaloneHost()->displayAudioError; + if (ae) + { + ae(errorText); + } } +void StandaloneHost::guaranteeRtAudioDAC() +{ + if (!rtaDac) + { + LOG << "Creating RtAudio DAC" << std::endl; + rtaDac = std::make_unique(RtAudio::UNSPECIFIED, &rtaErrorCallback); + rtaDac->showWarnings(true); + } +} + +std::tuple StandaloneHost::getDefaultAudioInOutSampleRate() +{ + guaranteeRtAudioDAC(); + auto iid = rtaDac->getDefaultInputDevice(); + auto oid = rtaDac->getDefaultOutputDevice(); + auto outInfo = rtaDac->getDeviceInfo(oid); + auto sr = outInfo.preferredSampleRate; + + return {iid, oid, (int32_t)sr}; +} void StandaloneHost::startAudioThread() { - rtaDac = std::make_unique(RtAudio::UNSPECIFIED, &rtaErrorCallback); - rtaDac->showWarnings(true); + guaranteeRtAudioDAC(); + + auto [in, out, sr] = getDefaultAudioInOutSampleRate(); + startAudioThreadOn(in, 2, numAudioInputs > 0, out, 2, numAudioOutputs > 0, sr); +} + +std::vector filterDevicesBy(const std::unique_ptr &rtaDac, + std::function f) +{ + std::vector res; auto dids = rtaDac->getDeviceIds(); auto dnms = rtaDac->getDeviceNames(); + for (auto d : dids) + { + auto inf = rtaDac->getDeviceInfo(d); + if (f(inf)) + { + res.push_back(inf); + } + } + return res; +} - RtAudio::StreamParameters oParams; - oParams.deviceId = rtaDac->getDefaultOutputDevice(); - auto outInfo = rtaDac->getDeviceInfo(oParams.deviceId); - oParams.nChannels = std::min(2U, outInfo.outputChannels); - oParams.firstChannel = 0; +std::vector StandaloneHost::getInputAudioDevices() +{ + guaranteeRtAudioDAC(); + return filterDevicesBy(rtaDac, [](auto &a) { return a.inputChannels > 0; }); +} - RtAudio::StreamParameters iParams; - iParams.deviceId = rtaDac->getDefaultInputDevice(); - auto inInfo = rtaDac->getDeviceInfo(iParams.deviceId); - iParams.nChannels = std::min(2U, inInfo.inputChannels); - iParams.firstChannel = 0; +std::vector StandaloneHost::getOutputAudioDevices() +{ + guaranteeRtAudioDAC(); + return filterDevicesBy(rtaDac, [](auto &a) { return a.outputChannels > 0; }); +} - LOG << "RtAudio Attached Devices" << std::endl; - for (auto i = 0U; i < dids.size(); ++i) +void StandaloneHost::startAudioThreadOn(unsigned int inputDeviceID, uint32_t inputChannels, + bool useInput, unsigned int outputDeviceID, + uint32_t outputChannels, bool useOutput, int32_t reqSampleRate) +{ + guaranteeRtAudioDAC(); + + if (rtaDac->isStreamRunning()) { - if (oParams.deviceId == dids[i]) LOG << " - Output : '" << dnms[i] << "'" << std::endl; + stopAudioThread(); + running = true; + finishedRunning = false; } - if (numAudioOutputs > 0) - for (auto i = 0U; i < dids.size(); ++i) + + audioInputDeviceID = inputDeviceID; + audioInputUsed = useInput; + audioOutputDeviceID = outputDeviceID; + audioOutputUsed = useOutput; + + auto dids = rtaDac->getDeviceIds(); + auto dnms = rtaDac->getDeviceNames(); + + RtAudio::StreamParameters oParams; + int32_t sampleRate{reqSampleRate}; + + if (useOutput) + { + oParams.deviceId = outputDeviceID; + auto outInfo = rtaDac->getDeviceInfo(oParams.deviceId); + oParams.nChannels = std::min(outputChannels, outInfo.outputChannels); + oParams.firstChannel = 0; + if (sampleRate < 0) { - if (iParams.deviceId == dids[i]) LOG << " - Input : '" << dnms[i] << "'" << std::endl; + sampleRate = outInfo.preferredSampleRate; + } + else + { + // Mkae sure this sample rate is available + bool isPossible{false}; + for (auto sr : outInfo.sampleRates) + { + isPossible = isPossible || ((int)sr == (int)sampleRate); + } + if (!isPossible) + { + sampleRate = outInfo.preferredSampleRate; + } } + } + + RtAudio::StreamParameters iParams; + if (useInput) + { + iParams.deviceId = inputDeviceID; + auto inInfo = rtaDac->getDeviceInfo(iParams.deviceId); + iParams.nChannels = std::min(inputChannels, inInfo.inputChannels); + iParams.firstChannel = 0; + if (sampleRate < 0) sampleRate = inInfo.preferredSampleRate; + } + + if (sampleRate < 0) + { + LOG << "No preferred sample rate detected; using 48k" << std::endl; + sampleRate = 48000; + } + + currentSampleRate = sampleRate; RtAudio::StreamOptions options; options.flags = RTAUDIO_SCHEDULE_REALTIME; + /* + * RTAudio doesn't tell you what the possible frame sizes are but instead + * just tells you to try open stream with power of twos you want. So leave + * this for now at 256 and return to it shortly. + */ + LOG << "[WARNING] Hardcoding frame size to 256 samples for now" << std::endl; uint32_t bufferFrames{256}; - if (rtaDac->openStream(&oParams, (numAudioInputs > 0) ? &iParams : nullptr, RTAUDIO_FLOAT32, 48000, - &bufferFrames, &rtaCallback, (void *)this, &options)) + if (rtaDac->openStream((useOutput) ? &oParams : nullptr, (useInput) ? &iParams : nullptr, + RTAUDIO_FLOAT32, sampleRate, &bufferFrames, &rtaCallback, (void *)this, + &options)) { LOG << "[ERROR]" << rtaDac->getErrorText() << std::endl; + rtaDac->closeStream(); return; } + activatePlugin(sampleRate, 1, bufferFrames * 2); + + LOG << "RtAudio Attached Devices" << std::endl; + if (useOutput) + { + for (auto i = 0U; i < dids.size(); ++i) + { + if (oParams.deviceId == dids[i]) LOG << " - Output : '" << dnms[i] << "'" << std::endl; + } + } + if (useInput) + { + for (auto i = 0U; i < dids.size(); ++i) + { + if (iParams.deviceId == dids[i]) LOG << " - Input : '" << dnms[i] << "'" << std::endl; + } + } + if (!rtaDac->isStreamOpen()) { + LOG << "[ERROR] Stream failed to open : " << rtaDac->getErrorText() << std::endl; return; } - if (!rtaDac->startStream()) + if (rtaDac->startStream()) { + LOG << "[ERROR] startStream failed : " << rtaDac->getErrorText() << std::endl; return; } - LOG << "RTA Started Stream" << std::endl; + LOG << "RtAudio: Started Stream" << std::endl; } void StandaloneHost::stopAudioThread() @@ -104,11 +232,14 @@ void StandaloneHost::stopAudioThread() { using namespace std::chrono_literals; std::this_thread::sleep_for(1ms); - // todo put a sleep here } LOG << "Audio Thread acknowledges shutdown" << std::endl; - if (rtaDac && rtaDac->isStreamRunning()) rtaDac->stopStream(); + if (rtaDac && rtaDac->isStreamRunning()) + { + rtaDac->stopStream(); + rtaDac->closeStream(); + } LOG << "RtAudio stream stopped" << std::endl; } return;