summaryrefslogtreecommitdiff
path: root/oss_write.c
blob: 4f6d548c0707562e9b95e3d76e9743e7d9e9de61 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
/* SPDX-License-Identifier: GPL-2.0 */

/** \file oss_write.c Paraslash's oss output plugin. */

#include <sys/ioctl.h>
#include <sys/soundcard.h>
#include <lopsub.h>

#include "write_cmd.lsg.h"
#include "para.h"
#include "fd.h"
#include "string.h"
#include "list.h"
#include "sched.h"
#include "buffer_tree.h"
#include "write.h"
#include "error.h"

/** Data specific to the oss writer. */
struct private_oss_write_data {
	/** The file handle of the device. */
	int fd;
	/** Four bytes for stereo streams, two bytes for mono streams. */
	int bytes_per_frame;
};

/*
 * We keep one bit of static storage to make sure only one instance of the oss
 * writer is running at any given time.
 */
static bool sound_device_busy;

static bool sound_device_is_busy(void)
{
	return sound_device_busy;
}

static void set_sound_device_busy(void)
{
	assert(!sound_device_busy);
	sound_device_busy = true;
}

static void set_sound_device_idle(void)
{
	assert(sound_device_busy);
	sound_device_busy = false;
}

static int get_oss_format(enum sample_format sf)
{
	switch (sf) {
	case SF_S8: return AFMT_S8;
	case SF_U8: return AFMT_U8;
	case SF_S16_LE: return AFMT_S16_LE;
	case SF_S16_BE: return AFMT_S16_BE;
	case SF_U16_LE: return AFMT_U16_LE;
	case SF_U16_BE: return AFMT_U16_BE;
	default: return -E_BAD_SAMPLE_FORMAT;
	}
}

static void oss_pre_monitor(struct sched *s, void *context)
{
	struct writer_node *wn = context;
	struct private_oss_write_data *powd = wn->private_data;
	int ret = btr_node_status(wn->btrn, wn->min_iqs, BTR_NT_LEAF);

	if (ret == 0 || (sound_device_is_busy() && !powd))
		return;
	if (ret < 0 || !powd)
		return sched_min_delay(s);
	sched_monitor_writefd(powd->fd, s);
}

static void oss_close(struct writer_node *wn)
{
	struct private_oss_write_data *powd = wn->private_data;

	if (!powd)
		return;
	close(powd->fd);
	free(powd);
	set_sound_device_idle();
}

/*
 * The Open Sound System Programmer's Guide sayeth:
 *
 * Set sampling parameters always so that number of channels (mono/stereo) is
 * set before selecting sampling rate (speed).  Failing to do this will make
 * your program incompatible with cards such as the SoundBlaster Pro which
 * supports 44.1 kHz in mono but just 22.05 kHz in stereo. A program which
 * selects 44.1 kHz speed and then sets the device to stereo mode will
 * incorrectly believe that the device is still in 44.1 kHz mode when actually
 * the speed is decreased to 22.05 kHz.
 */
static int oss_init(struct writer_node *wn, unsigned sample_rate,
		unsigned channels, int sample_format)
{
	int ret, format;
	unsigned ch, rate;
	struct private_oss_write_data *powd = zalloc(sizeof(*powd));
	const char *dev = WRITE_CMD_OPT_STRING_VAL(OSS, DEVICE, wn->lpr);

	PARA_INFO_LOG("opening %s\n", dev);
	ret = para_open(dev, O_WRONLY, 0);
	if (ret < 0)
		goto err_free;
	powd->fd = ret;
	ret = mark_fd_nonblocking(powd->fd);
	if (ret < 0)
		goto err;
	/* set PCM format */
	ret = get_oss_format(sample_format);
	if (ret < 0)
		return ret;
	sample_format = format = ret;
	ret = ioctl(powd->fd, SNDCTL_DSP_SETFMT, &format);
	if (ret < 0) {
		ret = -ERRNO_TO_PARA_ERROR(errno);
		PARA_ERROR_LOG("could not set sample format\n");
		goto err;
	}
	ret = -E_BAD_SAMPLE_FORMAT;
	if (format != sample_format)
		goto err;
	/* set number of channels */
	ch = channels;
	ret = ioctl(powd->fd, SNDCTL_DSP_CHANNELS, &ch);
	if (ret < 0) {
		ret = -ERRNO_TO_PARA_ERROR(errno);
		goto err;
	}
	ret = -E_BAD_CHANNEL_COUNT;
	if (ch != channels)
		goto err;
	if (format == SF_U8 || format == SF_S8)
		powd->bytes_per_frame = ch;
	else
		powd->bytes_per_frame = ch * 2;

	/*
	 * Set sampling rate
	 *
	 * If we request a higher sampling rate than is supported by the
	 * device, the highest possible speed is automatically used. The
	 * value actually used is returned as the new value of the argument.
	 */
	rate = sample_rate;
	ret = ioctl(powd->fd, SNDCTL_DSP_SPEED, &rate);
	if (ret < 0) {
		ret = -ERRNO_TO_PARA_ERROR(errno);
		goto err;
	}
	if (rate != sample_rate) {
		unsigned min = PARA_MIN(rate, sample_rate),
			max = PARA_MAX(rate, sample_rate);
		/*
		 * Check whether the returned sample rate differs significantly
		 * from the requested one.
		 */
		ret = -E_BAD_SAMPLERATE;
		if (100 * max > 110 * min) /* more than 10% deviation */
			goto err;
		PARA_NOTICE_LOG("using %uHz rather than %uHz\n", rate,
			sample_rate);
	}
	wn->min_iqs = powd->bytes_per_frame;
	wn->private_data = powd;
	return 1;
err:
	close(powd->fd);
err_free:
	free(powd);
	PARA_ERROR_LOG("failed to init %s: %s\n", dev, para_strerror(-ret));
	return ret;
}

static int oss_post_monitor(__a_unused struct sched *s, void *context)
{
	struct writer_node *wn = context;
	struct private_oss_write_data *powd = wn->private_data;
	struct btr_node *btrn = wn->btrn;
	size_t frames, bytes;
	int ret;
	char *data;
	audio_buf_info abi;

	ret = task_get_notification(wn->task);
	if (ret < 0)
		goto out;
	ret = btr_node_status(btrn, wn->min_iqs, BTR_NT_LEAF);
	if (ret <= 0)
		goto out;
	if (!powd) {
		int32_t rate, ch, format;

		if (sound_device_is_busy())
			return 0;
		ret = get_btr_sample_rate(btrn, &rate);
		if (ret < 0)
			goto out;
		ret = get_btr_channels(btrn, &ch);
		if (ret < 0)
			goto out;
		ret = get_btr_sample_format(btrn, &format);
		if (ret < 0)
			goto out;
		ret = oss_init(wn, rate, ch, format);
		if (ret < 0)
			goto out;
		set_sound_device_busy();
		return 0;
	}
	btr_merge(btrn, wn->min_iqs);
	bytes = btr_next_buffer(btrn, &data);
	frames = bytes / powd->bytes_per_frame;
	if (!frames) { /* eof and less than a single frame available */
		ret = -E_EOF;
		goto out;
	}
	ret = 0;
	if (!sched_write_ok(powd->fd, s))
		goto out;
	/* get maximal number of bytes that can be written */
	ret = ioctl(powd->fd, SNDCTL_DSP_GETOSPACE, &abi);
	if (ret >= 0) {
		size_t max_frames = abi.bytes / powd->bytes_per_frame;
		if (max_frames == 0)
			goto out;
		/* cap number of frames to avoid sound artefacts */
		frames = PARA_MIN(frames, max_frames);
	}
	ret = xwrite(powd->fd, data, frames * powd->bytes_per_frame);
	if (ret < 0)
		goto out;
	btr_consume(btrn, ret);
	ret = 0;
out:
	if (ret < 0)
		btr_remove_node(&wn->btrn);
	return ret;
}

/** \cond doxygen_ignore */
const struct writer lsg_write_cmd_com_oss_user_data = {
	.pre_monitor = oss_pre_monitor,
	.post_monitor = oss_post_monitor,
	.close = oss_close,
};
/** \endcond */