diff options
author | William McPherson <willmcpherson2@gmail.com> | 2020-02-27 01:01:17 +1100 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2020-02-27 10:21:13 +0100 |
commit | 72cbbd5297c47a96949de824c7e5bfa568b470b5 (patch) | |
tree | b0d90691f954c63775614563a7e7a8c87c252288 /Applications | |
parent | b1ed57e84b73460b52b034a56c20d5766ccf89e2 (diff) | |
download | serenity-72cbbd5297c47a96949de824c7e5bfa568b470b5.zip |
Piano: New timing system and zoomable piano roll
This patch allows roll notes to be of different sizes. This necessitates
a new internal representation of time. BPM and time signatures are
mostly implemented but not exposed.
Roll notes are now sample-accurate and the grid is aligned to 60 BPM
4/4. The roll is divided by the time signature raised to some power of
2, giving the musical divisions of (in the case of 4/4) 16, 32, 64 etc.
Before, our timing was derived from the buffer size and we relied on
that to implement delay. Delay has been rewritten to be sample-granular.
It's now exposed as the proper "divisions of a beat".
Something to be wary of is that the last buffer in the loop is also used
for the start of the next loop. In other words, we loop mid-buffer. This
means we write WAVs with a tiny bit of silence due to breaking the loop
after filling half a buffer.
The data structure for the roll is an array of SinglyLinkedLists of
RollNotes. Separating by pitch (via the array layout) makes insertion
much simpler and faster. Using sorted lists (and thus
SinglyLinkedListIterators) to do lookups is very quick as you know the
sample of the next note and can just compare it to the current sample. I
implemented this with HashMaps and the cost of lookups was abysmal. I
also tried a single SinglyLinkedList and the insertion code got even
more complicated than it already is.
Diffstat (limited to 'Applications')
-rw-r--r-- | Applications/Piano/AudioEngine.cpp | 115 | ||||
-rw-r--r-- | Applications/Piano/AudioEngine.h | 29 | ||||
-rw-r--r-- | Applications/Piano/KnobsWidget.cpp | 8 | ||||
-rw-r--r-- | Applications/Piano/MainWidget.cpp | 4 | ||||
-rw-r--r-- | Applications/Piano/Music.h | 12 | ||||
-rw-r--r-- | Applications/Piano/RollWidget.cpp | 128 | ||||
-rw-r--r-- | Applications/Piano/RollWidget.h | 6 | ||||
-rw-r--r-- | Applications/Piano/main.cpp | 6 |
8 files changed, 216 insertions, 92 deletions
diff --git a/Applications/Piano/AudioEngine.cpp b/Applications/Piano/AudioEngine.cpp index 50a7f57ce8..39da5d9930 100644 --- a/Applications/Piano/AudioEngine.cpp +++ b/Applications/Piano/AudioEngine.cpp @@ -46,11 +46,19 @@ void AudioEngine::fill_buffer(FixedArray<Sample>& buffer) { memset(buffer.data(), 0, buffer_size); - if (m_time == 0) - set_notes_from_roll(); - for (size_t i = 0; i < buffer.size(); ++i) { for (size_t note = 0; note < note_count; ++note) { + if (!m_roll_iters[note].is_end()) { + if (m_roll_iters[note]->on_sample == m_time) { + set_note(note, On); + } else if (m_roll_iters[note]->off_sample == m_time) { + set_note(note, Off); + ++m_roll_iters[note]; + if (m_roll_iters[note].is_end()) + m_roll_iters[note] = m_roll_notes[note].begin(); + } + } + switch (m_envelope[note]) { case Done: continue; @@ -104,25 +112,21 @@ void AudioEngine::fill_buffer(FixedArray<Sample>& buffer) buffer[i].left += sample.left * m_power[note] * volume; buffer[i].right += sample.right * m_power[note] * volume; } - } - if (m_delay) { - if (m_delay_buffers.size() >= static_cast<size_t>(m_delay)) { - auto to_blend = m_delay_buffers.dequeue(); - for (size_t i = 0; i < to_blend->size(); ++i) { - buffer[i].left += (*to_blend)[i].left * 0.333333; - buffer[i].right += (*to_blend)[i].right * 0.333333; - } + if (m_delay) { + buffer[i].left += m_delay_buffer[m_delay_index].left * 0.333333; + buffer[i].right += m_delay_buffer[m_delay_index].right * 0.333333; + m_delay_buffer[m_delay_index].left = buffer[i].left; + m_delay_buffer[m_delay_index].right = buffer[i].right; + if (++m_delay_index >= m_delay_samples) + m_delay_index = 0; } - auto delay_buffer = make<FixedArray<Sample>>(buffer.size()); - memcpy(delay_buffer->data(), buffer.data(), buffer_size); - m_delay_buffers.enqueue(move(delay_buffer)); - } - - if (++m_time == m_tick) { - m_time = 0; - update_roll(); + if (++m_time >= roll_length) { + m_time = 0; + if (!m_should_loop) + break; + } } memcpy(m_back_buffer_ptr->data(), buffer.data(), buffer_size); @@ -136,15 +140,14 @@ void AudioEngine::reset() m_front_buffer_ptr = &m_front_buffer; m_back_buffer_ptr = &m_back_buffer; - m_delay_buffers.clear(); + memset(m_delay_buffer.data(), 0, m_delay_buffer.size() * sizeof(Sample)); + m_delay_index = 0; memset(m_note_on, 0, sizeof(m_note_on)); memset(m_power, 0, sizeof(m_power)); memset(m_envelope, 0, sizeof(m_envelope)); m_time = 0; - m_current_column = 0; - m_previous_column = horizontal_notes - 1; } String AudioEngine::set_recorded_sample(const StringView& path) @@ -280,33 +283,54 @@ void AudioEngine::set_note_current_octave(int note, Switch switch_note) set_note(note + octave_base(), switch_note); } -void AudioEngine::set_roll_note(int y, int x, Switch switch_note) +void AudioEngine::sync_roll(int note) { - ASSERT(x >= 0 && x < horizontal_notes); - ASSERT(y >= 0 && y < note_count); - - m_roll_notes[y][x] = switch_note; - - if (x == m_current_column && switch_note == Off) // If you turn off a note that is playing. - set_note((note_count - 1) - y, Off); + auto it = m_roll_notes[note].find([&](auto& roll_note) { return roll_note.off_sample > m_time; }); + if (it.is_end()) + m_roll_iters[note] = m_roll_notes[note].begin(); + else + m_roll_iters[note] = it; } -void AudioEngine::update_roll() +void AudioEngine::set_roll_note(int note, u32 on_sample, u32 off_sample) { - if (++m_current_column == horizontal_notes) - m_current_column = 0; - if (++m_previous_column == horizontal_notes) - m_previous_column = 0; -} + RollNote new_roll_note = { on_sample, off_sample }; -void AudioEngine::set_notes_from_roll() -{ - for (int note = 0; note < note_count; ++note) { - if (m_roll_notes[note][m_previous_column] == On) - set_note((note_count - 1) - note, Off); - if (m_roll_notes[note][m_current_column] == On) - set_note((note_count - 1) - note, On); + ASSERT(note >= 0 && note < note_count); + ASSERT(new_roll_note.off_sample < roll_length); + ASSERT(new_roll_note.length() >= 2); + + for (auto it = m_roll_notes[note].begin(); !it.is_end();) { + if (it->on_sample > new_roll_note.off_sample) { + m_roll_notes[note].insert_before(it, new_roll_note); + sync_roll(note); + return; + } + if (it->on_sample == new_roll_note.on_sample && it->off_sample == new_roll_note.off_sample) { + if (m_time >= it->on_sample && m_time <= it->off_sample) + set_note(note, Off); + m_roll_notes[note].remove(it); + sync_roll(note); + return; + } + if ((new_roll_note.on_sample == 0 || it->on_sample >= new_roll_note.on_sample - 1) && it->on_sample <= new_roll_note.off_sample) { + if (m_time >= new_roll_note.off_sample && m_time <= it->off_sample) + set_note(note, Off); + m_roll_notes[note].remove(it); + it = m_roll_notes[note].begin(); + continue; + } + if (it->on_sample < new_roll_note.on_sample && it->off_sample >= new_roll_note.on_sample) { + if (m_time >= new_roll_note.off_sample && m_time <= it->off_sample) + set_note(note, Off); + it->off_sample = new_roll_note.on_sample - 1; + ASSERT(it->length() >= 2); + } + ++it; } + + m_roll_notes[note].append(new_roll_note); + sync_roll(note); } void AudioEngine::set_octave(Direction direction) @@ -373,6 +397,9 @@ void AudioEngine::set_release(int release) void AudioEngine::set_delay(int delay) { ASSERT(delay >= 0); - m_delay_buffers.clear(); m_delay = delay; + m_delay_samples = m_delay == 0 ? 0 : (sample_rate / (beats_per_minute / 60)) / m_delay; + m_delay_buffer.resize(m_delay_samples); + memset(m_delay_buffer.data(), 0, m_delay_buffer.size() * sizeof(Sample)); + m_delay_index = 0; } diff --git a/Applications/Piano/AudioEngine.h b/Applications/Piano/AudioEngine.h index 922278a3fa..aae9554520 100644 --- a/Applications/Piano/AudioEngine.h +++ b/Applications/Piano/AudioEngine.h @@ -30,9 +30,11 @@ #include "Music.h" #include <AK/FixedArray.h> #include <AK/Noncopyable.h> -#include <AK/Queue.h> +#include <AK/SinglyLinkedList.h> #include <LibAudio/Buffer.h> +typedef AK::SinglyLinkedListIterator<SinglyLinkedList<RollNote>, RollNote> RollIter; + class AudioEngine { AK_MAKE_NONCOPYABLE(AudioEngine) AK_MAKE_NONMOVABLE(AudioEngine) @@ -42,8 +44,7 @@ public: const FixedArray<Sample>& buffer() const { return *m_front_buffer_ptr; } const Vector<Audio::Sample>& recorded_sample() const { return m_recorded_sample; } - Switch roll_note(int y, int x) const { return m_roll_notes[y][x]; } - int current_column() const { return m_current_column; } + const SinglyLinkedList<RollNote>& roll_notes(int note) const { return m_roll_notes[note]; } int octave() const { return m_octave; } int octave_base() const { return (m_octave - octave_min) * 12; } int wave() const { return m_wave; } @@ -53,14 +54,14 @@ public: int release() const { return m_release; } int delay() const { return m_delay; } int time() const { return m_time; } - int tick() const { return m_tick; } void fill_buffer(FixedArray<Sample>& buffer); void reset(); + void set_should_loop(bool b) { m_should_loop = b; } String set_recorded_sample(const StringView& path); void set_note(int note, Switch); void set_note_current_octave(int note, Switch); - void set_roll_note(int y, int x, Switch); + void set_roll_note(int note, u32 on_sample, u32 off_sample); void set_octave(Direction); void set_wave(int wave); void set_wave(Direction); @@ -78,9 +79,7 @@ private: Audio::Sample noise() const; Audio::Sample recorded_sample(size_t note); - void update_roll(); - void set_notes_from_roll(); - + void sync_roll(int note); void set_sustain_impl(int sustain); FixedArray<Sample> m_front_buffer { sample_count }; @@ -88,7 +87,7 @@ private: FixedArray<Sample>* m_front_buffer_ptr { &m_front_buffer }; FixedArray<Sample>* m_back_buffer_ptr { &m_back_buffer }; - Queue<NonnullOwnPtr<FixedArray<Sample>>> m_delay_buffers; + Vector<Sample> m_delay_buffer; Vector<Audio::Sample> m_recorded_sample; @@ -108,11 +107,13 @@ private: int m_release; double m_release_step[note_count]; int m_delay { 0 }; + size_t m_delay_samples { 0 }; + size_t m_delay_index { 0 }; + + u32 m_time { 0 }; - int m_time { 0 }; - int m_tick { 8 }; + bool m_should_loop { true }; - Switch m_roll_notes[note_count][horizontal_notes] { { Off } }; - int m_current_column { 0 }; - int m_previous_column { horizontal_notes - 1 }; + SinglyLinkedList<RollNote> m_roll_notes[note_count]; + RollIter m_roll_iters[note_count]; }; diff --git a/Applications/Piano/KnobsWidget.cpp b/Applications/Piano/KnobsWidget.cpp index 0a4bf4b02f..5fc10c4fb3 100644 --- a/Applications/Piano/KnobsWidget.cpp +++ b/Applications/Piano/KnobsWidget.cpp @@ -63,7 +63,7 @@ KnobsWidget::KnobsWidget(AudioEngine& audio_engine, MainWidget& main_widget) m_decay_value = m_values_container->add<GUI::Label>(String::number(m_audio_engine.decay())); m_sustain_value = m_values_container->add<GUI::Label>(String::number(m_audio_engine.sustain())); m_release_value = m_values_container->add<GUI::Label>(String::number(m_audio_engine.release())); - m_delay_value = m_values_container->add<GUI::Label>(String::number(m_audio_engine.delay() / m_audio_engine.tick())); + m_delay_value = m_values_container->add<GUI::Label>(String::number(m_audio_engine.delay())); m_knobs_container = add<GUI::Widget>(); m_knobs_container->set_layout(make<GUI::HorizontalBoxLayout>()); @@ -144,12 +144,12 @@ KnobsWidget::KnobsWidget(AudioEngine& audio_engine, MainWidget& main_widget) constexpr int max_delay = 8; m_delay_knob = m_knobs_container->add<GUI::VerticalSlider>(); m_delay_knob->set_range(0, max_delay); - m_delay_knob->set_value(max_delay - (m_audio_engine.delay() / m_audio_engine.tick())); + m_delay_knob->set_value(max_delay - m_audio_engine.delay()); m_delay_knob->on_value_changed = [this](int value) { - int new_delay = m_audio_engine.tick() * (max_delay - value); + int new_delay = max_delay - value; m_audio_engine.set_delay(new_delay); ASSERT(new_delay == m_audio_engine.delay()); - m_delay_value->set_text(String::number(new_delay / m_audio_engine.tick())); + m_delay_value->set_text(String::number(new_delay)); }; } diff --git a/Applications/Piano/MainWidget.cpp b/Applications/Piano/MainWidget.cpp index d8eab8d8ad..6010a51fe4 100644 --- a/Applications/Piano/MainWidget.cpp +++ b/Applications/Piano/MainWidget.cpp @@ -79,9 +79,7 @@ MainWidget::~MainWidget() void MainWidget::custom_event(Core::CustomEvent&) { m_wave_widget->update(); - - if (m_audio_engine.time() == 0) - m_roll_widget->update(); + m_roll_widget->update(); } void MainWidget::keydown_event(GUI::KeyEvent& event) diff --git a/Applications/Piano/Music.h b/Applications/Piano/Music.h index c6a2c4fd7a..c2736c067e 100644 --- a/Applications/Piano/Music.h +++ b/Applications/Piano/Music.h @@ -56,6 +56,13 @@ enum Switch { On, }; +struct RollNote { + u32 length() const { return (off_sample - on_sample) + 1; } + + u32 on_sample; + u32 off_sample; +}; + enum Direction { Down, Up, @@ -196,7 +203,10 @@ constexpr int black_keys_per_octave = 5; constexpr int octave_min = 1; constexpr int octave_max = 7; -constexpr int horizontal_notes = 32; +constexpr double beats_per_minute = 60; +constexpr int beats_per_bar = 4; +constexpr int notes_per_beat = 4; +constexpr int roll_length = (sample_rate / (beats_per_minute / 60)) * beats_per_bar; // Equal temperament, A = 440Hz // We calculate note frequencies relative to A4: diff --git a/Applications/Piano/RollWidget.cpp b/Applications/Piano/RollWidget.cpp index 03b87413ac..b46989b0f1 100644 --- a/Applications/Piano/RollWidget.cpp +++ b/Applications/Piano/RollWidget.cpp @@ -29,9 +29,13 @@ #include "AudioEngine.h" #include <LibGUI/Painter.h> #include <LibGUI/ScrollBar.h> +#include <math.h> constexpr int note_height = 20; +constexpr int max_note_width = note_height * 2; constexpr int roll_height = note_count * note_height; +constexpr int horizontal_scroll_sensitivity = 20; +constexpr int max_zoom = 1 << 8; RollWidget::RollWidget(AudioEngine& audio_engine) : m_audio_engine(audio_engine) @@ -47,10 +51,21 @@ RollWidget::~RollWidget() void RollWidget::paint_event(GUI::PaintEvent& event) { - int roll_width = widget_inner_rect().width(); - double note_width = static_cast<double>(roll_width) / horizontal_notes; - - set_content_size({ roll_width, roll_height }); + m_roll_width = widget_inner_rect().width() * m_zoom_level; + set_content_size({ m_roll_width, roll_height }); + + // Divide the roll by the maximum note width. If we get fewer notes than + // our time signature requires, round up. Otherwise, round down to the + // nearest x*(2^y), where x is the base number of notes of our time + // signature. In other words, find a number that is a double of our time + // signature. For 4/4 that would be 16, 32, 64, 128 ... + m_num_notes = m_roll_width / max_note_width; + int time_signature_notes = beats_per_bar * notes_per_beat; + if (m_num_notes < time_signature_notes) + m_num_notes = time_signature_notes; + else + m_num_notes = time_signature_notes * pow(2, static_cast<int>(log2(m_num_notes / time_signature_notes))); + m_note_width = static_cast<double>(m_roll_width) / m_num_notes; // This calculates the minimum number of rows needed. We account for a // partial row at the top and/or bottom. @@ -63,25 +78,29 @@ void RollWidget::paint_event(GUI::PaintEvent& event) int notes_to_paint = paint_area / note_height; int key_pattern_index = (notes_per_octave - 1) - (note_offset % notes_per_octave); + int x_offset = horizontal_scrollbar().value(); + int horizontal_note_offset_remainder = fmod(x_offset, m_note_width); + int horizontal_paint_area = widget_inner_rect().width() + horizontal_note_offset_remainder; + if (fmod(horizontal_paint_area, m_note_width) != 0) + horizontal_paint_area += m_note_width; + int horizontal_notes_to_paint = horizontal_paint_area / m_note_width; + GUI::Painter painter(*this); painter.translate(frame_thickness(), frame_thickness()); - painter.translate(0, -note_offset_remainder); + painter.translate(-horizontal_note_offset_remainder, -note_offset_remainder); + painter.add_clip_rect(event.rect()); for (int y = 0; y < notes_to_paint; ++y) { int y_pos = y * note_height; - for (int x = 0; x < horizontal_notes; ++x) { + for (int x = 0; x < horizontal_notes_to_paint; ++x) { // This is needed to avoid rounding errors. You can't just use - // note_width as the width. - int x_pos = x * note_width; - int next_x_pos = (x + 1) * note_width; + // m_note_width as the width. + int x_pos = x * m_note_width; + int next_x_pos = (x + 1) * m_note_width; int distance_to_next_x = next_x_pos - x_pos; Gfx::Rect rect(x_pos, y_pos, distance_to_next_x, note_height); - if (m_audio_engine.roll_note(y + note_offset, x) == On) - painter.fill_rect(rect, note_pressed_color); - else if (x == m_audio_engine.current_column()) - painter.fill_rect(rect, column_playing_color); - else if (key_pattern[key_pattern_index] == Black) + if (key_pattern[key_pattern_index] == Black) painter.fill_rect(rect, Color::LightGray); else painter.fill_rect(rect, Color::White); @@ -94,6 +113,31 @@ void RollWidget::paint_event(GUI::PaintEvent& event) key_pattern_index = notes_per_octave - 1; } + painter.translate(-x_offset, -y_offset); + painter.translate(horizontal_note_offset_remainder, note_offset_remainder); + + for (int note = note_count - (note_offset + notes_to_paint); note <= (note_count - 1) - note_offset; ++note) { + for (auto roll_note : m_audio_engine.roll_notes(note)) { + int x = m_roll_width * (static_cast<double>(roll_note.on_sample) / roll_length); + int width = m_roll_width * (static_cast<double>(roll_note.length()) / roll_length); + if (x + width < x_offset || x > x_offset + widget_inner_rect().width()) + continue; + if (width < 2) + width = 2; + + int y = ((note_count - 1) - note) * note_height; + int height = note_height; + + Gfx::Rect rect(x, y, width, height); + painter.fill_rect(rect, note_pressed_color); + painter.draw_rect(rect, Color::Black); + } + } + + int x = m_roll_width * (static_cast<double>(m_audio_engine.time()) / roll_length); + if (x > x_offset && x <= x_offset + widget_inner_rect().width()) + painter.draw_line({ x, 0 }, { x, roll_height }, Gfx::Color::Black); + GUI::Frame::paint_event(event); } @@ -102,25 +146,61 @@ void RollWidget::mousedown_event(GUI::MouseEvent& event) if (!widget_inner_rect().contains(event.x(), event.y())) return; - int roll_width = widget_inner_rect().width(); - double note_width = static_cast<double>(roll_width) / horizontal_notes; - int y = (event.y() + vertical_scrollbar().value()) - frame_thickness(); y /= note_height; - // There's a case where we can't just use x / note_width. For example, if - // your note_width is 3.1 you will have a rect starting at 3. When that + // There's a case where we can't just use x / m_note_width. For example, if + // your m_note_width is 3.1 you will have a rect starting at 3. When that // leftmost pixel of the rect is clicked you will do 3 / 3.1 which is 0 - // and not 1. We can avoid that case by shifting x by 1 if note_width is + // and not 1. We can avoid that case by shifting x by 1 if m_note_width is // fractional, being careful not to shift out of bounds. - int x = event.x() - frame_thickness(); - bool note_width_is_fractional = note_width - static_cast<int>(note_width) != 0; + int x = (event.x() + horizontal_scrollbar().value()) - frame_thickness(); + bool note_width_is_fractional = m_note_width - static_cast<int>(m_note_width) != 0; bool x_is_not_last = x != widget_inner_rect().width() - 1; if (note_width_is_fractional && x_is_not_last) ++x; - x /= note_width; + x /= m_note_width; - m_audio_engine.set_roll_note(y, x, m_audio_engine.roll_note(y, x) == On ? Off : On); + int note = (note_count - 1) - y; + u32 on_sample = roll_length * (static_cast<double>(x) / m_num_notes); + u32 off_sample = (roll_length * (static_cast<double>(x + 1) / m_num_notes)) - 1; + m_audio_engine.set_roll_note(note, on_sample, off_sample); update(); } + +// FIXME: Implement zoom and horizontal scroll events in LibGUI, not here. +void RollWidget::mousewheel_event(GUI::MouseEvent& event) +{ + if (event.modifiers() & KeyModifier::Mod_Shift) { + horizontal_scrollbar().set_value(horizontal_scrollbar().value() + (event.wheel_delta() * horizontal_scroll_sensitivity)); + return; + } + + if (!(event.modifiers() & KeyModifier::Mod_Ctrl)) { + GUI::ScrollableWidget::mousewheel_event(event); + return; + } + + double multiplier = event.wheel_delta() >= 0 ? 0.5 : 2; + + if (m_zoom_level * multiplier > max_zoom) + return; + + if (m_zoom_level * multiplier < 1) { + if (m_zoom_level == 1) + return; + m_zoom_level = 1; + } else { + m_zoom_level *= multiplier; + } + + int absolute_x_of_pixel_at_cursor = horizontal_scrollbar().value() + event.position().x(); + int absolute_x_of_pixel_at_cursor_after_resize = absolute_x_of_pixel_at_cursor * multiplier; + int new_scrollbar = absolute_x_of_pixel_at_cursor_after_resize - event.position().x(); + + m_roll_width = widget_inner_rect().width() * m_zoom_level; + set_content_size({ m_roll_width, roll_height }); + + horizontal_scrollbar().set_value(new_scrollbar); +} diff --git a/Applications/Piano/RollWidget.h b/Applications/Piano/RollWidget.h index e83597931a..235d424412 100644 --- a/Applications/Piano/RollWidget.h +++ b/Applications/Piano/RollWidget.h @@ -42,6 +42,12 @@ private: virtual void paint_event(GUI::PaintEvent&) override; virtual void mousedown_event(GUI::MouseEvent& event) override; + virtual void mousewheel_event(GUI::MouseEvent&) override; AudioEngine& m_audio_engine; + + int m_roll_width; + int m_num_notes; + double m_note_width; + int m_zoom_level { 1 }; }; diff --git a/Applications/Piano/main.cpp b/Applications/Piano/main.cpp index 24e58a2522..13d1573422 100644 --- a/Applications/Piano/main.cpp +++ b/Applications/Piano/main.cpp @@ -80,11 +80,13 @@ int main(int argc, char** argv) if (need_to_write_wav) { need_to_write_wav = false; audio_engine.reset(); - while (audio_engine.current_column() < horizontal_notes - 1) { + audio_engine.set_should_loop(false); + do { audio_engine.fill_buffer(buffer); wav_writer.write_samples(reinterpret_cast<u8*>(buffer.data()), buffer_size); - } + } while (audio_engine.time()); audio_engine.reset(); + audio_engine.set_should_loop(true); wav_writer.finalize(); } } |