From c962c54610866b897c123c3695b063e0998dfd2d Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sat, 13 Jul 2019 17:05:16 +0200 Subject: [PATCH] Piano: Start working on a desktop piano. The idea here is to implement a simple synhesizer that allows you to play music with your keyboard. :^) It's a huge hack currently but we can improve upon this. --- Applications/Piano/Makefile | 23 ++ Applications/Piano/Music.h | 62 +++++ Applications/Piano/PianoWidget.cpp | 353 +++++++++++++++++++++++++++++ Applications/Piano/PianoWidget.h | 55 +++++ Applications/Piano/main.cpp | 35 +++ Kernel/build-root-filesystem.sh | 2 + Kernel/makeall.sh | 1 + 7 files changed, 531 insertions(+) create mode 100644 Applications/Piano/Makefile create mode 100644 Applications/Piano/Music.h create mode 100644 Applications/Piano/PianoWidget.cpp create mode 100644 Applications/Piano/PianoWidget.h create mode 100644 Applications/Piano/main.cpp diff --git a/Applications/Piano/Makefile b/Applications/Piano/Makefile new file mode 100644 index 0000000000..ed2c50af3f --- /dev/null +++ b/Applications/Piano/Makefile @@ -0,0 +1,23 @@ +include ../../Makefile.common + +OBJS = \ + PianoWidget.o \ + main.o + +APP = Piano + +DEFINES += -DUSERLAND + +all: $(APP) + +$(APP): $(OBJS) + $(LD) -o $(APP) $(LDFLAGS) $(OBJS) -lgui -lcore -lc + +.cpp.o: + @echo "CXX $<"; $(CXX) $(CXXFLAGS) -o $@ -c $< + +-include $(OBJS:%.o=%.d) + +clean: + @echo "CLEAN"; rm -f $(APP) $(OBJS) *.d + diff --git a/Applications/Piano/Music.h b/Applications/Piano/Music.h new file mode 100644 index 0000000000..098d04e0a2 --- /dev/null +++ b/Applications/Piano/Music.h @@ -0,0 +1,62 @@ +#pragma once + +#include + +namespace Music { + +struct Sample { + i16 left; + i16 right; +}; + +enum WaveType { Sine, Saw, Square, InvalidWave }; + +enum PianoKey { + K_C1, K_Db1, K_D1, K_Eb1, K_E1, K_F1, K_Gb1, K_G1, K_Ab1, K_A1, K_Bb1, K_B1, + K_C2, K_Db2, K_D2, K_Eb2, K_E2, K_F2, K_Gb2, K_G2, +}; + +inline bool is_white(PianoKey n) +{ + switch (n) { + case K_C1: + case K_D1: + case K_E1: + case K_F1: + case K_G1: + case K_A1: + case K_B1: + case K_C2: + case K_D2: + case K_E2: + case K_F2: + case K_G2: + return true; + default: + return false; + } +} + +enum Note { + C1, Db1, D1, Eb1, E1, F1, Gb1, G1, Ab1, A1, Bb1, B1, + C2, Db2, D2, Eb2, E2, F2, Gb2, G2, Ab2, A2, Bb2, B2, + C3, Db3, D3, Eb3, E3, F3, Gb3, G3, Ab3, A3, Bb3, B3, + C4, Db4, D4, Eb4, E4, F4, Gb4, G4, Ab4, A4, Bb4, B4, + C5, Db5, D5, Eb5, E5, F5, Gb5, G5, Ab5, A5, Bb5, B5, + C6, Db6, D6, Eb6, E6, F6, Gb6, G6, Ab6, A6, Bb6, B6, + C7, Db7, D7, Eb7, E7, F7, Gb7, G7, Ab7, A7, Bb7, B7, +}; + +const double note_frequency[] = { + /* Octave 1 */ 32.70, 34.65, 36.71, 38.89, 41.20, 43.65, 46.25, 49.00, 51.91, 55.00, 58.27, 61.74, + /* Octave 2 */ 65.41, 69.30, 73.42, 77.78, 82.41, 87.31, 92.50, 98.00, 103.83, 110.00, 116.54, 123.47, + /* Octave 3 */ 130.81, 138.59, 146.83, 155.56, 164.81, 174.61, 185.00, 196.00, 207.65, 220.00, 233.08, 246.94, + /* Octave 4 */ 261.63, 277.18, 293.66, 311.13, 329.63, 349.23, 369.99, 392.00, 415.30, 440.00, 466.16, 493.88, + /* Octave 5 */ 523.25, 554.37, 587.33, 622.25, 659.25, 698.46, 739.99, 783.99, 830.61, 880.00, 932.33, 987.77, + /* Octave 6 */ 1046.50, 1108.73, 1174.66, 1244.51, 1318.51, 1396.91, 1479.98, 1567.98, 1661.22, 1760.00, 1864.66, 1975.53, + /* Octave 7 */ 2093.00, 2217.46, 2349.32, 2489.02, 2637.02, 2793.83, 2959.96, 3135.96, 3322.44, 3520.00, 3729.31, 3951.07, +}; + +} + +using namespace Music; diff --git a/Applications/Piano/PianoWidget.cpp b/Applications/Piano/PianoWidget.cpp new file mode 100644 index 0000000000..c8ba4caa59 --- /dev/null +++ b/Applications/Piano/PianoWidget.cpp @@ -0,0 +1,353 @@ +#include "PianoWidget.h" +#include +#include +#include +#include + +PianoWidget::PianoWidget() +{ + memset(keys, 0, sizeof(keys)); + m_bitmap = GraphicsBitmap::create(GraphicsBitmap::Format::RGB32, { m_width, m_height }); +} + +PianoWidget::~PianoWidget() +{ +} + +void PianoWidget::paint_event(GPaintEvent& event) +{ + GPainter painter(*this); + painter.add_clip_rect(event.rect()); + painter.blit({ 0, 0 }, *m_bitmap, m_bitmap->rect()); +} + +void PianoWidget::fill_audio_buffer(uint8_t* stream, int len) +{ + size_t sample_count = len / sizeof(Sample); + memset(stream, 0, len); + + Sample* sst = (Sample*)stream; + for (size_t i = 0; i < sample_count; ++i) { + static const double VOLUME = 3000; + for (size_t n = 0; n < (sizeof(m_note_on) / sizeof(bool)); ++n) { + if (!m_note_on[n]) + continue; + double val = 0; + if (m_wave_type == WaveType::Sine) + val = ((VOLUME * m_power[n]) * w_sine(n)); + else if (m_wave_type == WaveType::Saw) + val = ((VOLUME * m_power[n]) * w_saw(n)); + else if (m_wave_type == WaveType::Square) + val = ((VOLUME * m_power[n]) * w_square(n)); + if (sst[i].left == 0) + sst[i].left = val; + else + sst[i].left += val; + } + sst[i].right = sst[i].left; + } + + // Release pressed notes. + if (m_release_enabled) { + for (size_t n = 0; n < (sizeof(m_note_on) / sizeof(bool)); ++n) { + if (m_note_on[n]) + m_power[n] *= 0.965; + } + } + + static Queue delay_frames; + static const int delay_length_in_frames = 50; + + //assert((int)sample_count == m_width); + + if (m_delay_enabled) { + if (delay_frames.size() >= delay_length_in_frames) { + auto* to_blend = delay_frames.dequeue(); + for (size_t i = 0; i < sample_count; ++i) { + sst[i].left += to_blend[i].left * 0.333333; + sst[i].right += to_blend[i].right * 0.333333; + } + delete[] to_blend; + } + Sample* frame = new Sample[sample_count]; + memcpy(frame, sst, sample_count * sizeof(Sample)); + + delay_frames.enqueue(frame); + } + + GPainter painter(*m_bitmap); + painter.fill_rect(m_bitmap->rect(), Color::Black); + + Sample* samples = (Sample*)stream; + Color wave_color; + if (m_wave_type == WaveType::Sine) + wave_color = Color(255, 192, 0); + else if (m_wave_type == WaveType::Saw) + wave_color = Color(240, 100, 128); + else if (m_wave_type == WaveType::Square) + wave_color = Color(128, 160, 255); + + int prev_x = 0; + int prev_y = m_height / 2; + for (int x = 0; x < (int)sample_count; ++x) { + double val = samples[x].left; + val /= 32768; + val *= m_height * 2; + int y = (m_height / 2) + val; + if (x == 0) + painter.set_pixel({ x, y }, wave_color); + else + painter.draw_line({ prev_x, prev_y }, { x, y }, wave_color); + prev_x = x; + prev_y = y; + } + + render_piano(painter); + render_knobs(painter); +} + +double PianoWidget::w_sine(size_t n) +{ + double pos = note_frequency[n] / 44100.0; + double sin_step = pos * 2 * M_PI; + double w = sin(m_sin_pos[n]); + m_sin_pos[n] += sin_step; + return w; +} + +static inline double hax_floor(double t) +{ + return (int)t; +} + +double PianoWidget::w_saw(size_t n) +{ + double saw_step = note_frequency[n] / 44100.0; + double t = m_saw_pos[n]; + double w = (0.5 - (t - hax_floor(t))) * 2; + //printf("w: %g, step: %g\n", w, saw_step); + m_saw_pos[n] += saw_step; + return w; +} + +double PianoWidget::w_square(size_t n) +{ + double pos = note_frequency[n] / 44100.0; + double square_step = pos * 2 * M_PI; + double w = sin(m_square_pos[n]); + if (w > 0) + w = 1; + else + w = -1; + //printf("w: %g, step: %g\n", w, square_step); + m_square_pos[n] += square_step; + return w; +} + +int PianoWidget::octave_base() const +{ + return (m_octave - m_octave_min) * 12; +} + +void PianoWidget::note(PianoKey offset_n, bool is_down) +{ + int n = offset_n + octave_base(); + if (m_note_on[n] == is_down) + return; + m_note_on[n] = is_down; + m_sin_pos[n] = 0; + m_saw_pos[n] = 0; + if (is_down) + m_power[n] = 1; + //printf("note[%u] = %u (%g)\n", (unsigned)n, is_down, note_frequency[n]); +} + +void PianoWidget::update_keys() +{ + note(K_C1, keys[KeyCode::Key_A]); + note(K_Db1, keys[KeyCode::Key_W]); + note(K_D1, keys[KeyCode::Key_S]); + note(K_Eb1, keys[KeyCode::Key_E]); + note(K_E1, keys[KeyCode::Key_D]); + note(K_F1, keys[KeyCode::Key_F]); + note(K_Gb1, keys[KeyCode::Key_T]); + note(K_G1, keys[KeyCode::Key_G]); + note(K_Ab1, keys[KeyCode::Key_Y]); + note(K_A1, keys[KeyCode::Key_H]); + note(K_Bb1, keys[KeyCode::Key_U]); + note(K_B1, keys[KeyCode::Key_J]); + note(K_C2, keys[KeyCode::Key_K]); + note(K_Db2, keys[KeyCode::Key_O]); + note(K_D2, keys[KeyCode::Key_L]); + note(K_Eb2, keys[KeyCode::Key_P]); + note(K_E2, keys[KeyCode::Key_Semicolon]); + note(K_F2, keys[KeyCode::Key_Apostrophe]); + note(K_Gb2, keys[KeyCode::Key_RightBracket]); + note(K_G2, keys[KeyCode::Key_Return]); +} + +void PianoWidget::keydown_event(GKeyEvent& event) +{ + switch (event.key()) { + case KeyCode::Key_C: + + if (++m_wave_type == InvalidWave) + m_wave_type = 0; + break; + case KeyCode::Key_V: + m_delay_enabled = !m_delay_enabled; + break; + case KeyCode::Key_B: + m_release_enabled = !m_release_enabled; + break; + case KeyCode::Key_Z: + if (m_octave > m_octave_min) + --m_octave; + memset(m_note_on, 0, sizeof(m_note_on)); + break; + case KeyCode::Key_X: + if (m_octave < m_octave_max) + ++m_octave; + memset(m_note_on, 0, sizeof(m_note_on)); + break; + } + + keys[event.key()] = true; + update_keys(); + update(); +} + +void PianoWidget::keyup_event(GKeyEvent& event) +{ + keys[event.key()] = false; + update_keys(); + update(); +} + + +static int white_key_width = 22; +static int white_key_height = 60; +static int black_key_width = 16; +static int black_key_height = 35; +static int black_key_stride = white_key_width - black_key_width; +static int black_key_offset = white_key_width - black_key_width / 2; + +void PianoWidget::render_piano_key(GPainter& painter, int index, PianoKey n, const StringView& text) +{ + Color color; + if (m_note_on[octave_base() + n]) { + color = Color(64, 64, 255); + } else { + if (is_white(n)) + color = Color::White; + else + color = Color::Black; + } + Rect rect; + int stride = 0; + int offset = 0; + if (is_white(n)) { + rect.set_width(white_key_width); + rect.set_height(white_key_height); + } else { + rect.set_width(black_key_width); + rect.set_height(black_key_height); + stride = black_key_stride; + offset = black_key_offset; + } + rect.set_x(offset + index * rect.width() + (index * stride)); + rect.set_y(m_height - white_key_height); + + painter.fill_rect(rect, color); + painter.draw_rect(rect, Color::Black); + + Color text_color; + if (is_white(n)) { + text_color = Color::Black; + } else { + text_color = Color::White; + } + Rect r(rect.x(), rect.y() + rect.height() / 2, rect.width(), rect.height() / 2); + painter.draw_text(r, text, TextAlignment::Center, text_color); +} + +void PianoWidget::render_piano(GPainter& painter) +{ + render_piano_key(painter, 0, K_C1, "A"); + render_piano_key(painter, 1, K_D1, "S"); + render_piano_key(painter, 2, K_E1, "D"); + render_piano_key(painter, 3, K_F1, "F"); + render_piano_key(painter, 4, K_G1, "G"); + render_piano_key(painter, 5, K_A1, "H"); + render_piano_key(painter, 6, K_B1, "J"); + render_piano_key(painter, 7, K_C2, "K"); + render_piano_key(painter, 8, K_D2, "L"); + render_piano_key(painter, 9, K_E2, ";"); + render_piano_key(painter, 10, K_F2, "'"); + render_piano_key(painter, 11, K_G2, "r"); + + render_piano_key(painter, 0, K_Db1, "W"); + render_piano_key(painter, 1, K_Eb1, "E"); + render_piano_key(painter, 3, K_Gb1, "T"); + render_piano_key(painter, 4, K_Ab1, "Y"); + render_piano_key(painter, 5, K_Bb1, "U"); + render_piano_key(painter, 7, K_Db2, "O"); + render_piano_key(painter, 8, K_Eb2, "P"); + render_piano_key(painter, 10, K_Gb2, "]"); +} + +static int knob_width = 100; + +void PianoWidget::render_knob(GPainter& painter, const Rect& rect, bool state, const StringView& text) +{ + Color text_color; + if (state) { + painter.fill_rect(rect, Color(0, 200, 0)); + text_color = Color::Black; + } else { + painter.draw_rect(rect, Color(180, 0, 0)); + text_color = Color(180, 0, 0); + } + painter.draw_text(rect, text, TextAlignment::Center, text_color); +} + +void PianoWidget::render_knobs(GPainter& painter) +{ + Rect delay_knob_rect(m_width - knob_width - 16, m_height - 50, knob_width, 16); + render_knob(painter, delay_knob_rect, m_delay_enabled, "V: Delay "); + + Rect release_knob_rect(m_width - knob_width - 16, m_height - 30, knob_width, 16); + render_knob(painter, release_knob_rect, m_release_enabled, "B: Release "); + + Rect octave_knob_rect(m_width - knob_width - 16 - knob_width - 16, m_height - 50, knob_width, 16); + auto text = String::format("Z/X: Oct %d ", m_octave); + int oct_rgb_step = 255 / (m_octave_max + 4); + int oshade = (m_octave + 4) * oct_rgb_step; + painter.draw_rect(octave_knob_rect, Color(oshade, oshade, oshade)); + painter.draw_text(octave_knob_rect, text, TextAlignment::Center, Color(oshade, oshade, oshade)); + + int r = 0, g = 0, b = 0; + if (m_wave_type == WaveType::Sine) { + r = 255; + g = 192; + b = 0; + } else if (m_wave_type == WaveType::Saw) { + r = 240; + g = 100; + b = 128; + } else if (m_wave_type == WaveType::Square) { + r = 128; + g = 160; + b = 255; + } + Rect wave_knob_rect(m_width - knob_width - 16 - knob_width - 16, m_height - 30, knob_width, 16); + const char* wave_name = ""; + if (m_wave_type == WaveType::Sine) + wave_name = "C: Sine "; + else if (m_wave_type == WaveType::Saw) + wave_name = "C: Sawtooth"; + else if (m_wave_type == WaveType::Square) + wave_name = "C: Square "; + painter.draw_rect(wave_knob_rect, Color(r, g, b)); + painter.draw_text(wave_knob_rect, wave_name, TextAlignment::Center, Color(r, g, b)); +} diff --git a/Applications/Piano/PianoWidget.h b/Applications/Piano/PianoWidget.h new file mode 100644 index 0000000000..05b3b5aea9 --- /dev/null +++ b/Applications/Piano/PianoWidget.h @@ -0,0 +1,55 @@ +#pragma once + +#include "Music.h" +#include + +class GPainter; + +class PianoWidget final : public GWidget { +public: + PianoWidget(); + virtual ~PianoWidget() override; + + virtual void paint_event(GPaintEvent&) override; + virtual void keydown_event(GKeyEvent&) override; + virtual void keyup_event(GKeyEvent&) override; + + void fill_audio_buffer(uint8_t* stream, int len); + +private: + double w_sine(size_t); + double w_saw(size_t); + double w_square(size_t); + + void render_piano_key(GPainter&, int index, PianoKey, const StringView&); + void render_piano(GPainter&); + void render_knobs(GPainter&); + void render_knob(GPainter&, const Rect&, bool state, const StringView&); + + void note(Music::PianoKey offset_n, bool is_down); + void update_keys(); + int octave_base() const; + + RefPtr m_bitmap; + +#define note_count sizeof(note_frequency) / sizeof(double) + + bool m_note_on[note_count]; + double m_power[note_count]; + double m_sin_pos[note_count]; + double m_square_pos[note_count]; + double m_saw_pos[note_count]; + + int m_octave_min { 1 }; + int m_octave_max { 6 }; + int m_octave { 4 }; + + int m_width { 512 }; + int m_height { 512 }; + + int m_wave_type { 0 }; + bool m_delay_enabled { false }; + bool m_release_enabled { false }; + + bool keys[256]; +}; diff --git a/Applications/Piano/main.cpp b/Applications/Piano/main.cpp new file mode 100644 index 0000000000..2085725741 --- /dev/null +++ b/Applications/Piano/main.cpp @@ -0,0 +1,35 @@ +#include "Music.h" +#include "PianoWidget.h" +#include +#include +#include +#include + +int main(int argc, char** argv) +{ + CFile audio("/dev/audio"); + if (!audio.open(CIODevice::WriteOnly)) { + dbgprintf("Can't open audio device: %s", audio.error_string()); + return 1; + } + + GApplication app(argc, argv); + + auto* window = new GWindow; + window->set_title("Piano"); + window->set_rect(100, 100, 512, 512); + + auto* piano_widget = new PianoWidget; + window->set_main_widget(piano_widget); + + window->show(); + + for (;;) { + GEventLoop::current().pump(GEventLoop::WaitMode::PollForEvents); + u8 buffer[4096]; + piano_widget->fill_audio_buffer(buffer, sizeof(buffer)); + audio.write(buffer, sizeof(buffer)); + } + + return 0; +} diff --git a/Kernel/build-root-filesystem.sh b/Kernel/build-root-filesystem.sh index d373006da5..b702d8b2ff 100755 --- a/Kernel/build-root-filesystem.sh +++ b/Kernel/build-root-filesystem.sh @@ -77,6 +77,7 @@ cp ../Applications/Terminal/Terminal mnt/bin/Terminal cp ../Applications/TextEditor/TextEditor mnt/bin/TextEditor cp ../Applications/PaintBrush/PaintBrush mnt/bin/PaintBrush cp ../Applications/QuickShow/QuickShow mnt/bin/QuickShow +cp ../Applications/Piano/Piano mnt/bin/Piano cp ../Demos/HelloWorld/HelloWorld mnt/bin/HelloWorld cp ../Demos/HelloWorld2/HelloWorld2 mnt/bin/HelloWorld2 cp ../Demos/RetroFetch/RetroFetch mnt/bin/RetroFetch @@ -108,6 +109,7 @@ ln -s WidgetGallery mnt/bin/wg ln -s TextEditor mnt/bin/te ln -s PaintBrush mnt/bin/pb ln -s QuickShow mnt/bin/qs +ln -s Piano mnt/bin/pi echo "done" # Run local sync script, if it exists diff --git a/Kernel/makeall.sh b/Kernel/makeall.sh index 301bdd18fe..5dc946ca1a 100755 --- a/Kernel/makeall.sh +++ b/Kernel/makeall.sh @@ -37,6 +37,7 @@ build_targets="$build_targets ../Applications/Taskbar" build_targets="$build_targets ../Applications/Downloader" build_targets="$build_targets ../Applications/PaintBrush" build_targets="$build_targets ../Applications/QuickShow" +build_targets="$build_targets ../Applications/Piano" build_targets="$build_targets ../DevTools/VisualBuilder" build_targets="$build_targets ../Games/Minesweeper" build_targets="$build_targets ../Games/Snake"