1 /*
2 * SPDX-License-Identifier: BSD-2-Clause
3 *
4 * Copyright (c) 2021 Goran Mekić
5 * Copyright (c) 2024 The FreeBSD Foundation
6 *
7 * Portions of this software were developed by Christos Margiolis
8 * <christos@FreeBSD.org> under sponsorship from the FreeBSD Foundation.
9 *
10 * Redistribution and use in source and binary forms, with or without
11 * modification, are permitted provided that the following conditions
12 * are met:
13 * 1. Redistributions of source code must retain the above copyright
14 * notice, this list of conditions and the following disclaimer.
15 * 2. Redistributions in binary form must reproduce the above copyright
16 * notice, this list of conditions and the following disclaimer in the
17 * documentation and/or other materials provided with the distribution.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
20 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
23 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
25 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
29 * SUCH DAMAGE.
30 */
31
32 #include <sys/soundcard.h>
33
34 #include <err.h>
35 #include <errno.h>
36 #include <fcntl.h>
37 #include <stdio.h>
38 #include <stdlib.h>
39 #include <string.h>
40 #include <unistd.h>
41
42 #ifndef SAMPLE_SIZE
43 #define SAMPLE_SIZE 16
44 #endif
45
46 /* Format can be unsigned, in which case replace S with U */
47 #if SAMPLE_SIZE == 32
48 typedef int32_t sample_t;
49 int format = AFMT_S32_NE; /* Signed 32bit native endian format */
50 #elif SAMPLE_SIZE == 16
51 typedef int16_t sample_t;
52 int format = AFMT_S16_NE; /* Signed 16bit native endian format */
53 #elif SAMPLE_SIZE == 8
54 typedef int8_t sample_t;
55 int format = AFMT_S8_NE; /* Signed 8bit native endian format */
56 #else
57 #error Unsupported sample format!
58 typedef int32_t sample_t;
59 int format = AFMT_S32_NE; /* Not a real value, just silencing
60 * compiler errors */
61 #endif
62
63 /*
64 * Minimal configuration for OSS
65 * For real world applications, this structure will probably contain many
66 * more fields
67 */
68 typedef struct config {
69 char *device;
70 int channels;
71 int fd;
72 int format;
73 int frag;
74 int sample_count;
75 int sample_rate;
76 int sample_size;
77 int chsamples;
78 int mmap;
79 oss_audioinfo audio_info;
80 audio_buf_info buffer_info;
81 } config_t;
82
83 /*
84 * Error state is indicated by value=-1 in which case application exits with
85 * error
86 */
87 static inline void
check_error(const int value,const char * message)88 check_error(const int value, const char *message)
89 {
90 if (value == -1)
91 err(1, "OSS error: %s\n", message);
92 }
93
94
95 /* Calculate frag by giving it minimal size of buffer */
96 static inline int
size2frag(int x)97 size2frag(int x)
98 {
99 int frag = 0;
100
101 while ((1 << frag) < x)
102 ++frag;
103
104 return (frag);
105 }
106
107 /*
108 * Split input buffer into channels. Input buffer is in interleaved format
109 * which means if we have 2 channels (L and R), this is what the buffer of 8
110 * samples would contain: L,R,L,R,L,R,L,R. The result are two channels
111 * containing: L,L,L,L and R,R,R,R.
112 */
113 static void
oss_split(config_t * config,sample_t * input,sample_t * output)114 oss_split(config_t *config, sample_t *input, sample_t *output)
115 {
116 int channel, index, i;
117
118 for (i = 0; i < config->sample_count; ++i) {
119 channel = i % config->channels;
120 index = i / config->channels;
121 output[channel * index] = input[i];
122 }
123 }
124
125 /*
126 * Convert channels into interleaved format and place it in output
127 * buffer
128 */
129 static void
oss_merge(config_t * config,sample_t * input,sample_t * output)130 oss_merge(config_t *config, sample_t *input, sample_t *output)
131 {
132 int channel, index;
133
134 for (channel = 0; channel < config->channels; ++channel) {
135 for (index = 0; index < config->chsamples; ++index) {
136 output[index * config->channels + channel] =
137 input[channel * index];
138 }
139 }
140 }
141
142 static void
oss_init(config_t * config)143 oss_init(config_t *config)
144 {
145 int error, tmp, min_frag;
146
147 /* Open the device for read and write */
148 config->fd = open(config->device, O_RDWR);
149 check_error(config->fd, "open");
150
151 /* Get device information */
152 config->audio_info.dev = -1;
153 error = ioctl(config->fd, SNDCTL_ENGINEINFO, &(config->audio_info));
154 check_error(error, "SNDCTL_ENGINEINFO");
155 printf("min_channels: %d\n", config->audio_info.min_channels);
156 printf("max_channels: %d\n", config->audio_info.max_channels);
157 printf("latency: %d\n", config->audio_info.latency);
158 printf("handle: %s\n", config->audio_info.handle);
159 if (config->audio_info.min_rate > config->sample_rate ||
160 config->sample_rate > config->audio_info.max_rate) {
161 errx(1, "%s doesn't support chosen samplerate of %dHz!\n",
162 config->device, config->sample_rate);
163 }
164 if (config->channels < 1)
165 config->channels = config->audio_info.max_channels;
166
167 /*
168 * If device is going to be used in mmap mode, disable all format
169 * conversions. Official OSS documentation states error code should not
170 * be checked.
171 * http://manuals.opensound.com/developer/mmap_test.c.html#LOC10
172 */
173 if (config->mmap) {
174 tmp = 0;
175 ioctl(config->fd, SNDCTL_DSP_COOKEDMODE, &tmp);
176 }
177
178 /*
179 * Set number of channels. If number of channels is chosen to the value
180 * near the one wanted, save it in config
181 */
182 tmp = config->channels;
183 error = ioctl(config->fd, SNDCTL_DSP_CHANNELS, &tmp);
184 check_error(error, "SNDCTL_DSP_CHANNELS");
185 /* Or check if tmp is close enough? */
186 if (tmp != config->channels) {
187 errx(1, "%s doesn't support chosen channel count of %d set "
188 "to %d!\n", config->device, config->channels, tmp);
189 }
190 config->channels = tmp;
191
192 /* Set format, or bit size: 8, 16, 24 or 32 bit sample */
193 tmp = config->format;
194 error = ioctl(config->fd, SNDCTL_DSP_SETFMT, &tmp);
195 check_error(error, "SNDCTL_DSP_SETFMT");
196 if (tmp != config->format) {
197 errx(1, "%s doesn't support chosen sample format!\n",
198 config->device);
199 }
200
201 /* Most common values for samplerate (in kHz): 44.1, 48, 88.2, 96 */
202 tmp = config->sample_rate;
203 error = ioctl(config->fd, SNDCTL_DSP_SPEED, &tmp);
204 check_error(error, "SNDCTL_DSP_SPEED");
205
206 /* Get and check device capabilities */
207 error = ioctl(config->fd, SNDCTL_DSP_GETCAPS, &(config->audio_info.caps));
208 check_error(error, "SNDCTL_DSP_GETCAPS");
209 if (!(config->audio_info.caps & PCM_CAP_DUPLEX))
210 errx(1, "Device doesn't support full duplex!\n");
211
212 if (config->mmap) {
213 if (!(config->audio_info.caps & PCM_CAP_TRIGGER))
214 errx(1, "Device doesn't support triggering!\n");
215 if (!(config->audio_info.caps & PCM_CAP_MMAP))
216 errx(1, "Device doesn't support mmap mode!\n");
217 }
218
219 /*
220 * If desired frag is smaller than minimum, based on number of channels
221 * and format (size in bits: 8, 16, 24, 32), set that as frag. Buffer
222 * size is 2^frag, but the real size of the buffer will be read when
223 * the configuration of the device is successful
224 */
225 min_frag = size2frag(config->sample_size * config->channels);
226
227 if (config->frag < min_frag)
228 config->frag = min_frag;
229
230 /*
231 * Allocate buffer in fragments. Total buffer will be split in number
232 * of fragments (2 by default)
233 */
234 if (config->buffer_info.fragments < 0)
235 config->buffer_info.fragments = 2;
236 tmp = ((config->buffer_info.fragments) << 16) | config->frag;
237 error = ioctl(config->fd, SNDCTL_DSP_SETFRAGMENT, &tmp);
238 check_error(error, "SNDCTL_DSP_SETFRAGMENT");
239
240 /* When all is set and ready to go, get the size of buffer */
241 error = ioctl(config->fd, SNDCTL_DSP_GETOSPACE, &(config->buffer_info));
242 check_error(error, "SNDCTL_DSP_GETOSPACE");
243 if (config->buffer_info.bytes < 1) {
244 errx(1, "OSS buffer error: buffer size can not be %d\n",
245 config->buffer_info.bytes);
246 }
247 config->sample_count = config->buffer_info.bytes / config->sample_size;
248 config->chsamples = config->sample_count / config->channels;
249 }
250
251 int
main(int argc,char * argv[])252 main(int argc, char *argv[])
253 {
254 int ret, bytes;
255 int8_t *ibuf, *obuf;
256 config_t config = {
257 .device = "/dev/dsp",
258 .channels = -1,
259 .format = format,
260 .frag = -1,
261 .sample_rate = 48000,
262 .sample_size = sizeof(sample_t),
263 .buffer_info.fragments = -1,
264 .mmap = 0,
265 };
266
267 /* Initialize device */
268 oss_init(&config);
269
270 /*
271 * Allocate input and output buffers so that their size match frag_size
272 */
273 bytes = config.buffer_info.bytes;
274 ibuf = malloc(bytes);
275 obuf = malloc(bytes);
276 sample_t *channels = malloc(bytes);
277
278 printf("bytes: %d, fragments: %d, fragsize: %d, fragstotal: %d, "
279 "samples: %d\n",
280 bytes, config.buffer_info.fragments,
281 config.buffer_info.fragsize, config.buffer_info.fragstotal,
282 config.sample_count);
283
284 /* Minimal engine: read input and copy it to the output */
285 for (;;) {
286 ret = read(config.fd, ibuf, bytes);
287 if (ret < bytes) {
288 fprintf(stderr, "Requested %d bytes, but read %d!\n",
289 bytes, ret);
290 break;
291 }
292 oss_split(&config, (sample_t *)ibuf, channels);
293 /* All processing will happen here */
294 oss_merge(&config, channels, (sample_t *)obuf);
295 ret = write(config.fd, obuf, bytes);
296 if (ret < bytes) {
297 fprintf(stderr, "Requested %d bytes, but wrote %d!\n",
298 bytes, ret);
299 break;
300 }
301 }
302
303 /* Cleanup */
304 free(channels);
305 free(obuf);
306 free(ibuf);
307 close(config.fd);
308
309 return (0);
310 }
311