1// This file is part of SmallBASIC
2//
3// Copyright(C) 2001-2020 Chris Warren-Smith.
4//
5// This program is distributed under the terms of the GPL v2.0 or later
6// Download the GNU Public License (GPL) from www.gnu.org
7//
8
9#define MINIAUDIO_IMPLEMENTATION
10#define DR_WAV_IMPLEMENTATION
11
12#include "config.h"
13#include <stdio.h>
14#include <stdint.h>
15#include <unistd.h>
16#include "include/osd.h"
17#include "ui/strlib.h"
18#include "ui/audio.h"
19#include "lib/miniaudio/extras/dr_wav.h"
20#include "lib/miniaudio/miniaudio.h"
21
22extern "C" {
23 uint32_t dev_get_millisecond_count();
24 void err_throw(const char *fmt, ...);
25}
26
27#define DEFAULT_FORMAT ma_format_f32
28#define DEFAULT_SAMPLE_RATE 44100
29#define DEFAULT_CHANNELS 1
30#define MILLIS_TO_MICROS(n) (n * 1000)
31
32struct Sound;
33static ma_context context;
34static ma_device device;
35static ma_device_config config;
36static strlib::Queue<Sound *> queue;
37static int queuePos = 0;
38
39struct Sound {
40 Sound(int frequency, int millis, int volume);
41 Sound();
42 virtual ~Sound();
43
44 ma_result construct(const char *path);
45
46 ma_waveform *_tone;
47 ma_decoder *_decoder;
48 uint32_t _start;
49 uint32_t _duration;
50};
51
52Sound::Sound() :
53 _tone(nullptr),
54 _decoder(nullptr),
55 _start(0),
56 _duration(0) {
57}
58
59Sound::Sound(int frequency, int millis, int volume) :
60 _tone(nullptr),
61 _decoder(nullptr),
62 _start(0),
63 _duration(millis) {
64 _tone = (ma_waveform *)malloc(sizeof(ma_waveform));
65 ma_waveform_config config =
66 ma_waveform_config_init(DEFAULT_FORMAT, DEFAULT_CHANNELS, DEFAULT_SAMPLE_RATE,
67 ma_waveform_type_sine, volume / 100.0, frequency);
68 ma_waveform_init(&config, _tone);
69}
70
71Sound::~Sound() {
72 if (_decoder) {
73 ma_decoder_uninit(_decoder);
74 free(_decoder);
75 _decoder = nullptr;
76 }
77 free(_tone);
78 _tone = nullptr;
79}
80
81ma_result Sound::construct(const char *path) {
82 _decoder = (ma_decoder *)malloc(sizeof(ma_decoder));
83 return _decoder == nullptr ? MA_OUT_OF_MEMORY : ma_decoder_init_file(path, nullptr, _decoder);
84}
85
86static void data_callback(ma_device *device, void *output, const void *input, ma_uint32 frameCount) {
87 if (queue.empty()) {
88 usleep(MILLIS_TO_MICROS(10));
89 } else {
90 queuePos = (queuePos + 1) % queue.size();
91 Sound *sound = queue[queuePos];
92 if (sound->_decoder != nullptr) {
93 // play audio track
94 ma_uint64 framesRead = ma_decoder_read_pcm_frames(sound->_decoder, output, frameCount);
95 if (framesRead == 0) {
96 // finished playing
97 queue.pop(false);
98 }
99 } else if (sound->_start == 0) {
100 // start new sound
101 sound->_start = dev_get_millisecond_count();
102 ma_waveform_read_pcm_frames(sound->_tone, (float *)output, frameCount);
103 } else if (dev_get_millisecond_count() - sound->_start > sound->_duration) {
104 // sound has timed out
105 queue.pop(false);
106 } else {
107 // continue sound
108 ma_waveform_read_pcm_frames(sound->_tone, (float *)output, frameCount);
109 }
110 }
111}
112
113static void setup_config(ma_format format, ma_uint32 channels, ma_uint32 sampleRate) {
114 config = ma_device_config_init(ma_device_type_playback);
115 config.playback.format = format;
116 config.playback.channels = channels;
117 config.sampleRate = sampleRate;
118 config.dataCallback = data_callback;
119 config.pUserData = nullptr;
120}
121
122static void setup_format(ma_format format, ma_uint32 channels, ma_uint32 sampleRate) {
123 if (config.playback.format != format ||
124 config.playback.channels != channels ||
125 config.sampleRate != sampleRate) {
126 audio_close();
127 setup_config(format, channels, sampleRate);
128 ma_result result = ma_device_init(&context, &config, &device);
129 if (result != MA_SUCCESS) {
130 err_throw("Failed to prepare sound device [%d]", result);
131 }
132 }
133}
134
135static void device_start() {
136 if (ma_device_get_state(&device) != MA_STATE_STARTED) {
137 ma_result result = ma_device_start(&device);
138 if (result != MA_SUCCESS) {
139 err_throw("Failed to start audio [%d]", result);
140 }
141 }
142}
143
144bool audio_open() {
145 bool result;
146 ma_backend backends[] = {
147 ma_backend_alsa,
148 ma_backend_jack,
149 ma_backend_pulseaudio,
150 ma_backend_wasapi,
151 ma_backend_dsound
152 };
153 if (ma_context_init(backends, sizeof(backends)/sizeof(backends[0]), NULL, &context) != MA_SUCCESS) {
154 result = false;
155 } else {
156 queuePos = 0;
157 setup_config(DEFAULT_FORMAT, DEFAULT_CHANNELS, DEFAULT_SAMPLE_RATE);
158 result = (ma_device_init(&context, &config, &device) == MA_SUCCESS);
159 }
160 return result;
161}
162
163void audio_close() {
164 osd_clear_sound_queue();
165 ma_device_uninit(&device);
166 ma_context_uninit(&context);
167}
168
169void osd_audio(const char *path) {
170 Sound *sound = new Sound();
171 ma_result result = sound->construct(path);
172 if (result != MA_SUCCESS) {
173 delete sound;
174 err_throw("Failed to open sound file [%d]", result);
175 } else {
176 setup_format(sound->_decoder->outputFormat,
177 sound->_decoder->outputChannels,
178 sound->_decoder->outputSampleRate);
179 ma_mutex_lock(&device.startStopLock);
180 queue.push(sound);
181 ma_mutex_unlock(&device.startStopLock);
182 device_start();
183 }
184}
185
186void osd_beep() {
187 osd_sound(1000, 30, 100, 0);
188 osd_sound(500, 30, 100, 0);
189}
190
191void osd_clear_sound_queue() {
192 ma_device_stop(&device);
193
194 // remove any cached sounds from the queue
195 List_each(Sound *, it, queue) {
196 Sound *next = *it;
197 if (next != NULL && next->_decoder != nullptr) {
198 queue.remove(it);
199 it--;
200 }
201 }
202
203 queue.removeAll();
204 queuePos = 0;
205}
206
207void osd_sound(int frequency, int millis, int volume, int background) {
208 setup_format(DEFAULT_FORMAT, DEFAULT_CHANNELS, DEFAULT_SAMPLE_RATE);
209 ma_mutex_lock(&device.startStopLock);
210 queue.push(new Sound(frequency, millis, volume));
211 ma_mutex_unlock(&device.startStopLock);
212
213 if (!background) {
214 device_start();
215 usleep(MILLIS_TO_MICROS(millis));
216 ma_device_stop(&device);
217 ma_mutex_lock(&device.startStopLock);
218 queue.pop();
219 ma_mutex_unlock(&device.startStopLock);
220 } else if (queue.size() == 1) {
221 device_start();
222 }
223}
224