| 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 |  | 
|---|
| 22 | extern "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 |  | 
|---|
| 32 | struct Sound; | 
|---|
| 33 | static ma_context context; | 
|---|
| 34 | static ma_device device; | 
|---|
| 35 | static ma_device_config config; | 
|---|
| 36 | static strlib::Queue<Sound *> queue; | 
|---|
| 37 | static int queuePos = 0; | 
|---|
| 38 |  | 
|---|
| 39 | struct 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 |  | 
|---|
| 52 | Sound::Sound() : | 
|---|
| 53 | _tone(nullptr), | 
|---|
| 54 | _decoder(nullptr), | 
|---|
| 55 | _start(0), | 
|---|
| 56 | _duration(0) { | 
|---|
| 57 | } | 
|---|
| 58 |  | 
|---|
| 59 | Sound::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 |  | 
|---|
| 71 | Sound::~Sound() { | 
|---|
| 72 | if (_decoder) { | 
|---|
| 73 | ma_decoder_uninit(_decoder); | 
|---|
| 74 | free(_decoder); | 
|---|
| 75 | _decoder = nullptr; | 
|---|
| 76 | } | 
|---|
| 77 | free(_tone); | 
|---|
| 78 | _tone = nullptr; | 
|---|
| 79 | } | 
|---|
| 80 |  | 
|---|
| 81 | ma_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 |  | 
|---|
| 86 | static 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 |  | 
|---|
| 113 | static 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 |  | 
|---|
| 122 | static 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 |  | 
|---|
| 135 | static 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 |  | 
|---|
| 144 | bool 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 |  | 
|---|
| 163 | void audio_close() { | 
|---|
| 164 | osd_clear_sound_queue(); | 
|---|
| 165 | ma_device_uninit(&device); | 
|---|
| 166 | ma_context_uninit(&context); | 
|---|
| 167 | } | 
|---|
| 168 |  | 
|---|
| 169 | void 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 |  | 
|---|
| 186 | void osd_beep() { | 
|---|
| 187 | osd_sound(1000, 30, 100, 0); | 
|---|
| 188 | osd_sound(500, 30, 100, 0); | 
|---|
| 189 | } | 
|---|
| 190 |  | 
|---|
| 191 | void 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 |  | 
|---|
| 207 | void 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 |  | 
|---|