Skip to content

Commit b3ede26

Browse files
committed
Add PulseAudio backend for SuperCollider on Linux
For a discussion of why we should have a PulseAudio backend for SuperCollider on Linux, see supercollider/rfcs#11. It is important to understand that this is not meant to be a replacement for jack at all. This is just to give an option on systems where jack is difficult to set up. jack will continue to be the default. This PR adds support for PulseAudio, both inputs and outputs. The device selection is done outside SC, as per PulseAudio philosophy, where applications output to a default sink, and get input from a default source, and the actual connections happen on the system's setup / PulseAudio control applications. The changes are pretty self contained, with just one C++ file added, and then some changes to CMakeLists and READMEs as required. The selection of frontend is done at build time, and by default the backend will continue to be jack. This will only try to use the PulseAudio backend if instructed explicitly to do so, using the AUDIOAPI cmake variable. So, for users that are currently happy with their setup, using jack, this change will not affect them at all and will be completely transparent. On systems where it is difficult to use jack, the user / maintainer can set the cmake variable to pulseaudio, and then, and only then, it will use the PulseAudio backend. As you can see the code is not too complex, and should be easy to maintain. Note that the expectation is that the PulseAudio backend will be used in simple-ish systems, so we may not want to support complex / exotic audio configurations, and encourage people to use jack for that.
1 parent e341b49 commit b3ede26

File tree

5 files changed

+343
-3
lines changed

5 files changed

+343
-3
lines changed

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ else() # ARM platforms do not have SSE
171171
set(SSE2 OFF)
172172
endif()
173173

174-
set(AUDIOAPI "default" CACHE STRING "Audio API to use (one of {default,coreaudio,jack,portaudio})")
174+
set(AUDIOAPI "default" CACHE STRING "Audio API to use (one of {default,coreaudio,jack,portaudio,pulseaudio})")
175175

176176
if (AUDIOAPI STREQUAL jack)
177177
# here we check for JACK metadata API

README_LINUX.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,14 @@ names are separated by ':' as in the Unix PATH variable:
318318
$> export SC_SYNTHDEF_PATH="./synthdefs:/home/sk/SuperCollider/synthdefs"
319319
```
320320

321+
Choosing an audio backend
322+
-------------------------
323+
324+
There are 2 audio backends available in Linux: jack and PulseAudio. **The recommended audio backend on Linux is jack, which is the one that will be built by default.**
325+
326+
On systems where it is difficult to make jack work, then it is possible to use the PulseAudio backend. For this, set the `AUDIOAPI` variable to `pulseaudio` in cmake (see "Step 3: Set CMake flags" above), and build normally. Also, make sure that the development libraries for PulseAudio and RtAudio are available before the build. On debian systems this might be done:
327+
328+
sudo apt-get install libpulse-dev librtaudio-dev
321329

322330
Contributors to this document
323331
-----------------------------
@@ -331,3 +339,4 @@ Contributors to this document
331339
- nescivi (marije baalman)
332340
- dan stowell
333341
- tim blechmann
342+
- luis lloret

lang/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ if(AUDIOAPI STREQUAL "default")
116116
endif(APPLE)
117117
endif()
118118

119-
if(NOT AUDIOAPI MATCHES "^(jack|coreaudio|portaudio)$")
119+
if(NOT AUDIOAPI MATCHES "^(jack|coreaudio|portaudio|pulseaudio)$")
120120
message(FATAL_ERROR "Unrecognised audio API: ${AUDIOAPI}")
121121
endif()
122122

server/scsynth/CMakeLists.txt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ if(AUDIOAPI STREQUAL "default")
1111
endif(APPLE)
1212
endif()
1313

14-
if(NOT AUDIOAPI MATCHES "^(jack|coreaudio|portaudio)$")
14+
if(NOT AUDIOAPI MATCHES "^(jack|coreaudio|portaudio|pulseaudio)$")
1515
message(FATAL_ERROR "Unrecognised audio API: ${AUDIOAPI}")
1616
endif()
1717

@@ -25,6 +25,11 @@ elseif(AUDIOAPI STREQUAL portaudio AND ((NOT WIN32) OR MSYS)) # MSYS like Apple
2525
if(NOT PORTAUDIO_FOUND)
2626
message(FATAL_ERROR "Portaudio selected as audio API, but development files not found")
2727
endif()
28+
elseif(AUDIOAPI STREQUAL pulseaudio)
29+
find_package(PulseAudio)
30+
if(NOT PULSEAUDIO_FOUND)
31+
message(FATAL_ERROR "Pulseaudio selected as audio API, but development files not found")
32+
endif()
2833
endif()
2934
message(STATUS "Audio API: ${AUDIOAPI}")
3035

@@ -101,6 +106,10 @@ elseif (AUDIOAPI STREQUAL portaudio)
101106
list(APPEND scsynth_sources SC_PortAudio.cpp ${CMAKE_SOURCE_DIR}/common/SC_PaUtils.cpp)
102107
add_definitions("-DSC_AUDIO_API=SC_AUDIO_API_PORTAUDIO" ${PORTAUDIO_DEFINITIONS})
103108
include_directories(${PORTAUDIO_INCLUDE_DIRS})
109+
elseif (AUDIOAPI STREQUAL pulseaudio)
110+
list(APPEND scsynth_sources SC_PulseAudio.cpp)
111+
add_definitions("-DSC_AUDIO_API=SC_AUDIO_API_PULSEAUDIO" ${PULSEAUDIO_DEFINITIONS})
112+
include_directories(${PULSEAUDIO_INCLUDE_DIRS})
104113
endif()
105114

106115
set (FINAL_BUILD 0) # disable final build for scsynth
@@ -174,6 +183,8 @@ elseif(AUDIOAPI STREQUAL portaudio)
174183
endif()
175184
elseif(AUDIOAPI STREQUAL coreaudio)
176185
target_link_libraries(libscsynth "-framework CoreAudio")
186+
elseif(AUDIOAPI STREQUAL pulseaudio)
187+
target_link_libraries(libscsynth ${PULSEAUDIO_LIBRARY} rtaudio)
177188
endif()
178189

179190
target_link_libraries(libscsynth boost_system_lib boost_filesystem_lib)

server/scsynth/SC_PulseAudio.cpp

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
/*
2+
SuperCollider real time audio synthesis system
3+
Copyright (c) 2002 James McCartney. All rights reserved.
4+
http://www.audiosynth.com
5+
6+
This program is free software; you can redistribute it and/or modify
7+
it under the terms of the GNU General Public License as published by
8+
the Free Software Foundation; either version 2 of the License, or
9+
(at your option) any later version.
10+
11+
This program is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU General Public License for more details.
15+
16+
You should have received a copy of the GNU General Public License
17+
along with this program; if not, write to the Free Software
18+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19+
*/
20+
#include <stdarg.h>
21+
#include <math.h>
22+
#include <stdlib.h>
23+
#include <rtaudio/RtAudio.h>
24+
25+
#include "SC_CoreAudio.h"
26+
#include "SC_Prototypes.h"
27+
#include "SC_HiddenWorld.h"
28+
#include "SC_WorldOptions.h"
29+
#include "SC_Time.hpp"
30+
31+
32+
int32 server_timeseed() { return timeSeed(); }
33+
34+
#include "SC_TimeDLL.hpp"
35+
// =====================================================================
36+
// Timing
37+
38+
static inline int64 sc_PAOSCTime() { return OSCTime(getTime()); }
39+
40+
static inline double sc_PAOSCTimeSeconds() { return (uint64)sc_PAOSCTime() * kOSCtoSecs; }
41+
42+
int64 oscTimeNow() { return sc_PAOSCTime(); }
43+
44+
void initializeScheduler() {}
45+
46+
47+
class SC_PulseAudioDriver : public SC_AudioDriver {
48+
int m_inputChannelCount, m_outputChannelCount;
49+
int64 m_paStreamStartupTime;
50+
int64 m_paStreamStartupTimeOSC;
51+
RtAudio* m_audio;
52+
double m_maxOutputLatency;
53+
SC_TimeDLL m_DLL;
54+
55+
int rtCallback(void* outputBuffer, void* inputBuffer, unsigned int nFrames, double streamTime,
56+
RtAudioStreamStatus status);
57+
static int rtCallbackStatic(void* outputBuffer, void* inputBuffer, unsigned int nFrames, double streamTime,
58+
RtAudioStreamStatus status, void* userData);
59+
60+
protected:
61+
// Driver interface methods
62+
virtual bool DriverSetup(int* outNumSamplesPerCallback, double* outSampleRate) override;
63+
virtual bool DriverStart() override;
64+
virtual bool DriverStop() override;
65+
66+
public:
67+
SC_PulseAudioDriver(struct World* inWorld);
68+
virtual ~SC_PulseAudioDriver();
69+
};
70+
71+
// This is the "entry point" for our code. This will be called by the server to initialize the audio backend
72+
SC_AudioDriver* SC_NewAudioDriver(struct World* inWorld) { return new SC_PulseAudioDriver(inWorld); }
73+
74+
SC_PulseAudioDriver::SC_PulseAudioDriver(struct World* inWorld): SC_AudioDriver(inWorld), m_maxOutputLatency(0.) {
75+
scprintf("In SC_PulseAudioDriver::SC_PulseAudioDriver\n");
76+
// We ask RtAudio to use the PulseAudio backend
77+
m_audio = new RtAudio(RtAudio::LINUX_PULSE);
78+
}
79+
80+
SC_PulseAudioDriver::~SC_PulseAudioDriver() {
81+
scprintf("In SC_PulseAudioDriver::~SC_PulseAudioDriver\n");
82+
m_audio->closeStream();
83+
delete m_audio;
84+
}
85+
86+
// This is the callback static entry point. It will just relay the call to the object's proper method,
87+
// using the userData pointer
88+
int SC_PulseAudioDriver::rtCallbackStatic(void* outputBuffer, void* inputBuffer, unsigned int nFrames,
89+
double streamTime, RtAudioStreamStatus status, void* userData) {
90+
SC_PulseAudioDriver* driver = (SC_PulseAudioDriver*)userData;
91+
92+
return driver->rtCallback(outputBuffer, inputBuffer, nFrames, streamTime, status);
93+
}
94+
95+
void sc_SetDenormalFlags();
96+
97+
// This is where all the data movement between SuperCollider and the sound server happens.
98+
// Since PulseAudio uses interleaved samples, that's what we use here
99+
int SC_PulseAudioDriver::rtCallback(void* outputBuffer, void* inputBuffer, unsigned int nFrames, double streamTime,
100+
RtAudioStreamStatus status) {
101+
sc_SetDenormalFlags();
102+
World* world = mWorld;
103+
104+
m_DLL.Update(sc_PAOSCTimeSeconds());
105+
106+
// This is where all the data movement takes place
107+
try {
108+
mFromEngine.Free();
109+
mToEngine.Perform();
110+
mOscPacketsToEngine.Perform();
111+
112+
int numSamples = NumSamplesPerCallback();
113+
int bufFrames = mWorld->mBufLength;
114+
int numBufs = numSamples / bufFrames;
115+
116+
if (nFrames != bufFrames) {
117+
scprintf("warning: nFrames in callback != mWorld->mBufLength\n");
118+
}
119+
120+
float* inBuses = mWorld->mAudioBus + mWorld->mNumOutputs * bufFrames;
121+
float* outBuses = mWorld->mAudioBus;
122+
int32* inTouched = mWorld->mAudioBusTouched + mWorld->mNumOutputs;
123+
int32* outTouched = mWorld->mAudioBusTouched;
124+
125+
int bufFramePos = 0;
126+
int64 oscTime = mOSCbuftime = (uint64)((m_DLL.PeriodTime() + m_maxOutputLatency) * kSecondsToOSCunits + .5);
127+
int64 oscInc = mOSCincrement = (uint64)((m_DLL.Period() / numBufs) * kSecondsToOSCunits + .5);
128+
mSmoothSampleRate = m_DLL.SampleRate();
129+
double oscToSamples = mOSCtoSamples = mSmoothSampleRate * kOSCtoSecs /* 1/pow(2,32) */;
130+
131+
// main loop
132+
for (int i = 0; i < numBufs; ++i, mWorld->mBufCounter++, bufFramePos += bufFrames) {
133+
int32 bufCounter = mWorld->mBufCounter;
134+
int32* tch;
135+
136+
// Process inputs, marking them as touched
137+
tch = inTouched;
138+
for (int k = 0; k < m_inputChannelCount; ++k) {
139+
*tch++ = bufCounter;
140+
float* dst = inBuses + k * bufFrames;
141+
const float* src = (float*)inputBuffer + k + bufFramePos;
142+
for (int n = 0; n < bufFrames; n++) {
143+
*dst = *src;
144+
src += m_outputChannelCount;
145+
dst++;
146+
}
147+
}
148+
149+
// run engine
150+
int64 schedTime;
151+
int64 nextTime = oscTime + oscInc;
152+
while ((schedTime = mScheduler.NextTime()) <= nextTime) {
153+
float diffTime = (float)(schedTime - oscTime) * oscToSamples + 0.5;
154+
float diffTimeFloor = floor(diffTime);
155+
world->mSampleOffset = (int)diffTimeFloor;
156+
world->mSubsampleOffset = diffTime - diffTimeFloor;
157+
158+
if (world->mSampleOffset < 0)
159+
world->mSampleOffset = 0;
160+
else if (world->mSampleOffset >= world->mBufLength)
161+
world->mSampleOffset = world->mBufLength - 1;
162+
163+
SC_ScheduledEvent event = mScheduler.Remove();
164+
event.Perform();
165+
}
166+
world->mSampleOffset = 0;
167+
world->mSubsampleOffset = 0.0f;
168+
169+
World_Run(world);
170+
171+
// Process outputs (considering which ones have been touched)
172+
tch = outTouched;
173+
for (int k = 0; k < m_outputChannelCount; ++k) {
174+
float* dst = (float*)outputBuffer + k + bufFramePos;
175+
if (tch[k] == bufCounter) {
176+
const float* src = outBuses + k * bufFrames;
177+
for (int n = 0; n < bufFrames; n++) {
178+
*dst = *src;
179+
src++;
180+
dst += m_outputChannelCount;
181+
}
182+
} else {
183+
*dst = 0;
184+
dst += m_outputChannelCount;
185+
}
186+
}
187+
// update buffer time
188+
oscTime = mOSCbuftime = nextTime;
189+
}
190+
} catch (std::exception& exc) {
191+
scprintf("SC_PulseAudioDriver: exception in real time: %s\n", exc.what());
192+
} catch (...) {
193+
scprintf("SC_PulseAudioDriver: unknown exception in real time\n");
194+
}
195+
#if 0
196+
double cpuUsage = Pa_GetStreamCpuLoad(mStream) * 100.0;
197+
mAvgCPU = mAvgCPU + 0.1 * (cpuUsage - mAvgCPU);
198+
if (cpuUsage > mPeakCPU || --mPeakCounter <= 0) {
199+
mPeakCPU = cpuUsage;
200+
mPeakCounter = mMaxPeakCounter;
201+
}
202+
203+
#endif
204+
mAudioSync.Signal();
205+
return 0;
206+
}
207+
208+
209+
// ========================================================================
210+
// The SC_AudioDriver interface methods that we need to implement are below
211+
212+
bool SC_PulseAudioDriver::DriverSetup(int* outNumSamples, double* outSampleRate) {
213+
int rc;
214+
int device;
215+
scprintf("In SC_PulseAudioDriver::DriverSetup\n");
216+
217+
// Show the devices
218+
int numDevices = m_audio->getDeviceCount();
219+
if (numDevices == 0) {
220+
scprintf("No audio devices found\n");
221+
return false;
222+
}
223+
224+
scprintf("Found %d device(s):\n", numDevices);
225+
std::vector<RtAudio::DeviceInfo> infos;
226+
for (int i = 0; i < numDevices; i++) {
227+
try {
228+
RtAudio::DeviceInfo info;
229+
info = m_audio->getDeviceInfo(i);
230+
scprintf(" - %s (device #%d with %d ins %d outs)\n", info.name.c_str(), i, info.inputChannels,
231+
info.outputChannels);
232+
233+
infos.push_back(info);
234+
} catch (RtAudioError& e) {
235+
e.printMessage();
236+
break;
237+
}
238+
}
239+
240+
// Use the default devices
241+
unsigned int outputDevice = m_audio->getDefaultOutputDevice();
242+
unsigned int inputDevice = m_audio->getDefaultInputDevice();
243+
244+
// Number of input and output channels to use
245+
m_outputChannelCount = std::min<size_t>(mWorld->mNumOutputs, infos[outputDevice].duplexChannels);
246+
m_inputChannelCount = std::min<size_t>(mWorld->mNumInputs, infos[inputDevice].duplexChannels);
247+
248+
// What sample rate to use? If we get one, make sure it is supported, and fall back to the default if not
249+
*outNumSamples = mWorld->mBufLength;
250+
if (mPreferredSampleRate) {
251+
std::vector<unsigned int> supportedSampleRates { infos[outputDevice].sampleRates };
252+
if (std::find(supportedSampleRates.begin(), supportedSampleRates.end(), (unsigned int)mPreferredSampleRate)
253+
!= supportedSampleRates.end()) {
254+
*outSampleRate = mPreferredSampleRate;
255+
} else {
256+
scprintf("Requested sample rate NOT supported. Setting to 44.1 kHz\n");
257+
*outSampleRate = 44100;
258+
}
259+
} else {
260+
*outSampleRate = 44100;
261+
}
262+
263+
// Configure the stream parameters
264+
RtAudio::StreamParameters outParameters;
265+
outParameters.deviceId = outputDevice;
266+
outParameters.nChannels = m_outputChannelCount;
267+
RtAudio::StreamParameters inParameters;
268+
inParameters.deviceId = inputDevice;
269+
inParameters.nChannels = m_inputChannelCount;
270+
// use a separate variable for the number of frames, so that we can compare with the actual given value
271+
// and warn if they are different
272+
unsigned int bufferFrames = *outNumSamples;
273+
RtAudio::StreamOptions options;
274+
options.flags = RTAUDIO_MINIMIZE_LATENCY | RTAUDIO_SCHEDULE_REALTIME;
275+
options.streamName = "SuperCollider";
276+
try {
277+
scprintf("Opening stream with %d output and %d input channels\n", outParameters.nChannels,
278+
inParameters.nChannels);
279+
// Depending on whether we are using inputs, we open the stream with the appropriate parameters
280+
if (m_inputChannelCount > 0) {
281+
m_audio->openStream(&outParameters, &inParameters, RTAUDIO_FLOAT32, *outSampleRate, &bufferFrames,
282+
&SC_PulseAudioDriver::rtCallbackStatic, this, &options);
283+
} else {
284+
m_audio->openStream(&outParameters, NULL, RTAUDIO_FLOAT32, *outSampleRate, &bufferFrames,
285+
&SC_PulseAudioDriver::rtCallbackStatic, this, &options);
286+
}
287+
// Check that the requested number of frames matches what SC asked for and issue a warning if not
288+
if (*outNumSamples != bufferFrames) {
289+
scprintf("*outNumSamples != bufferFrames (%d != %d)\n", *outNumSamples, bufferFrames);
290+
}
291+
*outNumSamples = bufferFrames;
292+
} catch (RtAudioError& e) {
293+
e.printMessage();
294+
return false;
295+
}
296+
return true;
297+
}
298+
299+
bool SC_PulseAudioDriver::DriverStart() {
300+
// sync times
301+
m_paStreamStartupTimeOSC = 0;
302+
m_paStreamStartupTime = 0;
303+
304+
// Start the streaming
305+
try {
306+
m_audio->startStream();
307+
} catch (RtAudioError& e) {
308+
e.printMessage();
309+
return false;
310+
}
311+
312+
m_DLL.Reset(mSampleRate, mNumSamplesPerCallback, SC_TIME_DLL_BW, sc_PAOSCTimeSeconds());
313+
return true;
314+
}
315+
316+
bool SC_PulseAudioDriver::DriverStop() {
317+
// We just stop the stream
318+
m_audio->stopStream();
319+
return true;
320+
}

0 commit comments

Comments
 (0)