summaryrefslogtreecommitdiff
path: root/doc
diff options
context:
space:
mode:
authorWim Taymans <wtaymans@redhat.com>2020-06-13 21:26:21 +0200
committerWim Taymans <wtaymans@redhat.com>2020-06-13 21:26:21 +0200
commit5e652612be3bcaf5ff5fc35ccddc567d7a2706d0 (patch)
tree520f67880797c56d698d56b74dd416bee11dfdc0 /doc
parent40ef322a3422aaeda6ffa2e071f3bf5d9c713969 (diff)
doc: add tutorial for pw_stream playback
Diffstat (limited to 'doc')
-rw-r--r--doc/tutorial-index.md3
-rw-r--r--doc/tutorial4.md301
2 files changed, 304 insertions, 0 deletions
diff --git a/doc/tutorial-index.md b/doc/tutorial-index.md
index f35d7537..be40820f 100644
--- a/doc/tutorial-index.md
+++ b/doc/tutorial-index.md
@@ -8,3 +8,6 @@ PipeWire API step-by-step with simple short examples.
2) Enumerating objects ([tutorial 2](tutorial2.md)).
3) Forcing a roundtrip ([tutorial 3](tutorial3.md)).
+
+4) Playing a tone with `pw_stream` ([tutorial 4](tutorial4.md)).
+
diff --git a/doc/tutorial4.md b/doc/tutorial4.md
new file mode 100644
index 00000000..024b3b20
--- /dev/null
+++ b/doc/tutorial4.md
@@ -0,0 +1,301 @@
+[[previous]](tutorial3.md) [[index]](tutorial-index.md) [[next]](tutorial5.md)
+
+# Playing a tone (Tutorial 4)
+
+In this tutorial we show how to use a stream to play a tone.
+
+Let's take a look at the code before we break it down:
+
+```c
+#include <math.h>
+
+#include <spa/param/audio/format-utils.h>
+
+#include <pipewire/pipewire.h>
+
+#define M_PI_M2 ( M_PI + M_PI )
+
+#define DEFAULT_RATE 44100
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_VOLUME 0.7
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_stream *stream;
+ double accumulator;
+};
+
+static void on_process(void *userdata)
+{
+ struct data *data = userdata;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ int i, c, n_frames, stride;
+ int16_t *dst, val;
+
+ if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) {
+ pw_log_warn("out of buffers: %m");
+ return;
+ }
+
+ buf = b->buffer;
+ if ((dst = buf->datas[0].data) == NULL)
+ return;
+
+ stride = sizeof(int16_t) * DEFAULT_CHANNELS;
+ n_frames = buf->datas[0].maxsize / stride;
+
+ for (i = 0; i < n_frames; i++) {
+ data->accumulator += M_PI_M2 * 440 / DEFAULT_RATE;
+ if (data->accumulator >= M_PI_M2)
+ data->accumulator -= M_PI_M2;
+
+ val = sin(data->accumulator) * DEFAULT_VOLUME * 16767.f;
+ for (c = 0; c < DEFAULT_CHANNELS; c++)
+ *dst++ = val;
+ }
+
+ buf->datas[0].chunk->offset = 0;
+ buf->datas[0].chunk->stride = stride;
+ buf->datas[0].chunk->size = n_frames * stride;
+
+ pw_stream_queue_buffer(data->stream, b);
+}
+
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .process = on_process,
+};
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+ pw_init(&argc, &argv);
+
+ data.loop = pw_main_loop_new(NULL);
+
+ data.stream = pw_stream_new_simple(
+ pw_main_loop_get_loop(data.loop),
+ "audio-src",
+ pw_properties_new(
+ PW_KEY_MEDIA_TYPE, "Audio",
+ PW_KEY_MEDIA_CATEGORY, "Playback",
+ PW_KEY_MEDIA_ROLE, "Music",
+ NULL),
+ &stream_events,
+ &data);
+
+ params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_S16,
+ .channels = DEFAULT_CHANNELS,
+ .rate = DEFAULT_RATE ));
+
+ pw_stream_connect(data.stream,
+ PW_DIRECTION_OUTPUT,
+ argc > 1 ? (uint32_t)atoi(argv[1]) : PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, 1);
+
+ pw_main_loop_run(data.loop);
+
+ pw_stream_destroy(data.stream);
+ pw_main_loop_destroy(data.loop);
+
+ return 0;
+}
+```
+
+Save as tutorial4.c and compile with:
+
+```
+gcc -Wall tutorial4.c -o tutorial4 -lm $(pkg-config --cflags --libs libpipewire-0.3)
+```
+
+We start with the usual boilerplate, `pw_init()` and a `pw_main_loop_new()`.
+We're going to store our objects in a structure so that we can pass them
+around in callbacks later.
+
+```c
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_stream *stream;
+ double accumulator;
+};
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+
+ pw_init(&argc, &argv);
+
+ data.loop = pw_main_loop_new(NULL);
+```
+
+Next we create a stream object. It takes the mainloop as first argument and
+a stream name as the second. Next we provide some properties for the stream
+and a callback + data.
+
+```c
+ data.stream = pw_stream_new_simple(
+ pw_main_loop_get_loop(data.loop),
+ "audio-src",
+ pw_properties_new(
+ PW_KEY_MEDIA_TYPE, "Audio",
+ PW_KEY_MEDIA_CATEGORY, "Playback",
+ PW_KEY_MEDIA_ROLE, "Music",
+ NULL),
+ &stream_events,
+ &data);
+```
+
+We using `pw_stream_new_simple()` but there is also a `pw_stream_new()` that
+takes an exising `struct pw_core` as the first argument and that requires you
+to add the event handle manually, for more control. The `pw_stream_new_simple()`
+is, as the name implies, easier to use because it creates a `struct pw_context`
+and `struct pw_core` automatically.
+
+In the properties we need to give as much information about the stream as we
+can so that the session manager can make good decisions about how and where
+to route this stream. There are 3 important properties to configure:
+
+* `PW_KEY_MEDIA_TYPE` The media type, like Audio, Video, Midi
+* `pw_KEY_MEDIA_CATEGORY` The category, like Playback, Capture, Duplex, Monitor
+* `PW_KEY_MEDIA_ROLE` The media role, like Movie, Music, Camera, Screen,
+ Communication, Game, Notification, DSP,
+ Production, Accessibility, Test
+
+The properties are owned by the stream and freed when the stream is destroyed
+later.
+
+This is the event structure that we use to listen for events:
+
+```c
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .process = on_process,
+};
+```
+
+We are for the moment only interested now in the `process` event. This event
+is called whenever we need to produce more data. We'll wee how that function
+is implemented but first we need to setup the format of the stream:
+
+```c
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+
+#define DEFAULT_RATE 44100
+#define DEFAULT_CHANNELS 2
+
+ params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &SPA_AUDIO_INFO_RAW_INIT(
+ .format = SPA_AUDIO_FORMAT_S16,
+ .channels = DEFAULT_CHANNELS,
+ .rate = DEFAULT_RATE ));
+```
+
+This is using a `struct spa_pod_builder` to make a `struct spa_pod *` object
+in the buffer array on the stack. The parameter is of type `SPA_PARAM_EnumFormat`
+which means that it enumerates the possible formats for this stream. We have
+only one, a Signed 16 bit stereo format at 44.1KHz.
+
+We use `spa_format_audio_raw_build()` which is a helper function to make the param
+with the builder. See [SPA POD](spa/POD.md) for more information about how to
+make these POD objects.
+
+Now we're ready to connect the stream and run the main loop:
+
+```c
+ pw_stream_connect(data.stream,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_MAP_BUFFERS |
+ PW_STREAM_FLAG_RT_PROCESS,
+ params, 1);
+
+ pw_main_loop_run(data.loop);
+```
+
+To connect we speficy that we have an `PW_DIRECTION_OUTPUT` stream. `PW_ID_ANY`
+means that we are ok with conneting to any consumer. Next we set some flags:
+
+* `PW_STREAM_FLAG_AUTOCONNECT` automatically connect this stream. This instructs
+ the session manager to link us to some consumer.
+* `PW_STREAM_FLAG_MAP_BUFFERS` mmap the buffers for us so we can access the
+ memory. If you don't set these flags you have
+ either work with the fd or mmap yourself.
+* `PW_STREAM_FLAG_RT_PROCESS` Run the process function in the realtime thread.
+ Only use this is the process function only
+ uses functions that are realtime safe, this means
+ no allocation or file access or locking.
+
+An last we pass the extra parameters for our stream. Here we only have the
+allowed formats (`SPA_PARAM_EnumFormat`).
+
+Running the mainloop will then start processing and will result in our
+`process` callback to be called. Let have a look at that function now.
+
+The main program flow of the process function is:
+
+* `pw_stream_dequeue_buffer()` to obtain a buffer to write into.
+* Get pointers in buffer memory to write to
+* write data into buffer
+* adjust buffer with number of written bytes, offset, stride,
+* `pw_stream_queue_buffer()` to queue the buffer for playback.
+
+```c
+static void on_process(void *userdata)
+{
+ struct data *data = userdata;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ int i, c, n_frames, stride;
+ int16_t *dst, val;
+
+ if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) {
+ pw_log_warn("out of buffers: %m");
+ return;
+ }
+
+ buf = b->buffer;
+ if ((dst = buf->datas[0].data) == NULL)
+ return;
+
+ stride = sizeof(int16_t) * DEFAULT_CHANNELS;
+ n_frames = buf->datas[0].maxsize / stride;
+
+ for (i = 0; i < n_frames; i++) {
+ data->accumulator += M_PI_M2 * 440 / DEFAULT_RATE;
+ if (data->accumulator >= M_PI_M2)
+ data->accumulator -= M_PI_M2;
+
+ val = sin(data->accumulator) * DEFAULT_VOLUME * 16767.f;
+ for (c = 0; c < DEFAULT_CHANNELS; c++)
+ *dst++ = val;
+ }
+
+ buf->datas[0].chunk->offset = 0;
+ buf->datas[0].chunk->stride = stride;
+ buf->datas[0].chunk->size = n_frames * stride;
+
+ pw_stream_queue_buffer(data->stream, b);
+}
+```
+
+Check out the docs for [buffers](spa/buffer.md) for more information
+about how to work with buffers.
+
+Try to change the number of channels, samplerate or format; the stream
+will automatically convert to the format on the server.
+
+
+[[previous]](tutorial3.md) [[index]](tutorial-index.md) [[next]](tutorial5.md)