10 Commits

Author SHA1 Message Date
2c3df4baed fix: buffer length parameters change 2023-06-18 13:45:14 +04:00
aeeab6977f refactor: minor changes in the code 2023-06-18 03:25:27 +04:00
1ac275906f feat: playing full notes with ring buffer 2023-06-18 02:31:45 +04:00
0eb203f9f4 feat: configure debugging 2023-06-17 19:37:18 +04:00
d7f4538418 fix(note): semitone shift overflow 2023-06-17 19:37:02 +04:00
0cdac55d27 wip: playing sounds on keypress 2023-06-17 16:33:38 +04:00
10bf0c6a06 feat: detect note pressed 2023-06-17 16:03:07 +04:00
320a3cc8e0 feat: raylib and it's window added (#4)
Co-authored-by: HiveBeats <e1lama@protonmail.com>
Reviewed-on: #4
2023-06-17 14:31:18 +03:00
a93278f705 feat: adsr documentation 2023-06-17 00:19:16 +04:00
46ac7c9bba feat: phase accum documentation 2023-06-17 00:12:56 +04:00
7 changed files with 473 additions and 30 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
.DS_Store
/Debug/
*.wav
*.dSYM

View File

@@ -4,5 +4,6 @@
],
"files.associations": {
"algorithm": "c"
}
},
"FSharp.suggestGitignore": false,
}

32
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,32 @@
{
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: clang сборка активного файла",
"command": "/usr/bin/clang",
"args": [
"-fcolor-diagnostics",
"-fansi-escape-codes",
"-g",
"${file}",
"${fileDirname}/parser.c",
"-lm",
"-lraylib",
"-o",
"${fileDirname}/bin/${fileBasenameNoExtension}"
],
"options": {
"cwd": "${fileDirname}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "Задача создана отладчиком."
}
],
"version": "2.0.0"
}

View File

@@ -1,3 +1,3 @@
#!/bin/bash
CC="${CXX:-cc}"
$CC -Wall -std=c11 ./main.c ./parser.c -lm -o ./bin/main
$CC -Wall -std=c11 ./main.c ./parser.c -lm -lraylib -o ./bin/main

60
docs/ADSR.md Normal file
View File

@@ -0,0 +1,60 @@
Certainly! Here's an example of generating an ADSR (Attack, Decay, Sustain, Release) envelope in Python:
```python
import numpy as np
import matplotlib.pyplot as plt
# Parameters
sample_rate = 44100 # Sample rate in Hz
duration = 5 # Duration of the envelope in seconds
# Time values
num_samples = int(duration * sample_rate)
time = np.arange(num_samples) / sample_rate
# ADSR parameters
attack_time = 0.5 # Attack time in seconds
decay_time = 0.3 # Decay time in seconds
sustain_level = 0.6 # Sustain level (0 to 1)
release_time = 1.0 # Release time in seconds
# Generate the ADSR envelope
envelope = np.zeros(num_samples)
attack_samples = int(attack_time * sample_rate)
decay_samples = int(decay_time * sample_rate)
release_samples = int(release_time * sample_rate)
# Attack phase
envelope[:attack_samples] = np.linspace(0, 1, num=attack_samples)
# Decay phase
decay_slope = (1 - sustain_level) / decay_samples
envelope[attack_samples:attack_samples + decay_samples] = np.linspace(1, sustain_level, num=decay_samples) - decay_slope * np.arange(decay_samples)
# Sustain phase
envelope[attack_samples + decay_samples:-release_samples] = sustain_level
# Release phase
release_slope = sustain_level / release_samples
envelope[-release_samples:] = sustain_level - release_slope * np.arange(release_samples)
# Normalize the envelope
envelope /= np.max(envelope)
# Plot the envelope
plt.plot(time, envelope)
plt.xlabel('Time (s)')
plt.ylabel('Amplitude')
plt.title('ADSR Envelope')
plt.show()
```
In this example, we specify the sample rate and duration of the envelope. We define the ADSR parameters: attack time, decay time, sustain level, and release time.
We create an array to store the envelope values and initialize it with zeros. We calculate the number of samples for each phase based on the sample rate and duration.
We then calculate the envelope values for each phase. The attack phase increases linearly from 0 to 1. The decay phase decreases linearly from 1 to the sustain level. The sustain phase maintains a constant value equal to the sustain level. The release phase decreases linearly from the sustain level to 0.
After generating the envelope, we normalize it to ensure the maximum value is 1. Finally, we plot the envelope using Matplotlib.
You can modify the ADSR parameters to create different envelope shapes or experiment with adding modulation to create more dynamic and expressive sounds.

39
docs/PhaseAccumulation.md Normal file
View File

@@ -0,0 +1,39 @@
Certainly! Here's a simple example of phase accumulation in Python code:
```python
import numpy as np
import matplotlib.pyplot as plt
# Parameters
sample_rate = 44100 # Sample rate in Hz
frequency = 440 # Frequency of the oscillator in Hz
duration = 1 # Duration of the generated waveform in seconds
# Calculate the phase increment per sample
phase_increment = 2 * np.pi * frequency / sample_rate
# Initialize phase and time arrays
num_samples = int(duration * sample_rate)
phase = np.zeros(num_samples)
time = np.arange(num_samples) / sample_rate
# Perform phase accumulation
for i in range(1, num_samples):
phase[i] = phase[i - 1] + phase_increment
# Generate the waveform (sine wave) based on the accumulated phase
waveform = np.sin(phase)
# Plot the waveform
plt.plot(time, waveform)
plt.xlabel('Time (s)')
plt.ylabel('Amplitude')
plt.title('Phase Accumulation Example - Sine Wave')
plt.show()
```
In this example, we specify the sample rate, frequency, and duration of the waveform. We calculate the phase increment per sample based on the desired frequency. Then, we initialize arrays to store the phase values and time values. By iterating through each sample, we perform phase accumulation by adding the phase increment to the previous phase value.
Finally, we generate a sine wave by taking the sine of the accumulated phase values. The resulting waveform is plotted using Matplotlib.
This code demonstrates the basic concept of phase accumulation, where the phase of an oscillator is incremented over time to generate a periodic waveform. You can modify the parameters, try different waveforms, or experiment with phase modulation to create more complex sounds.

364
main.c
View File

@@ -4,26 +4,148 @@
#include "math.h"
#include "parser.h"
#include "raylib.h"
#define SAMPLE_RATE 48000.f
#define BPM 120.f
#define BEAT_DURATION 60.f/BPM
#define PITCH_STANDARD 440.f
#define VOLUME 0.5f
#define ATTACK_MS 100.f
#define STREAM_BUFFER_SIZE 4096
#define PI 3.1415926535f
#define SYNTH_PI 3.1415926535f
#define SYNTH_VOLUME 0.5f
#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define write_log(format,args...) do { \
printf(format, ## args); \
} while(0)
//------------------------------------------------------------------------------------
// General Sound
// Ring Buffer
//------------------------------------------------------------------------------------
typedef struct Sound {
typedef struct RingBuffer {
float* items;
size_t head;
size_t tail;
int is_full;
int is_empty;
size_t size;
} RingBuffer;
RingBuffer ring_buffer_init(size_t buffer_size) {
RingBuffer buffer = {
.items = calloc(buffer_size, sizeof(float)),
.head = 0,
.tail = 0,
.is_full = 0,
.is_empty = 1,
.size = buffer_size
};
return buffer;
}
void ring_buffer_reset(RingBuffer* me) {
me->head = 0;
me->tail = 0;
me->is_full = 0;
}
// +
static void advance_pointer(RingBuffer* me) {
if(me->is_full) {
me->tail++;
if (me->tail == me->size) {
me->tail = 0;
}
}
me->head++;
if (me->head == me->size) {
me->head = 0;
}
size_t is_full = me->head == me->tail ? 1 : 0;
me->is_full = is_full;
}
// -
static void retreat_pointer(RingBuffer* me) {
me->is_full = 0;
me->tail++;
if (me->tail == me->size) {
me->tail = 0;
}
}
void ring_buffer_write(RingBuffer* buffer, float* data, size_t count) {
if (buffer->is_full || buffer->head + count > buffer->size) {
write_log("[WARN] Trying to overfill the ring buffer: \n\tIsFull:%d\n\tHead:%zu\n\tCount:%zu\n\t",
buffer->is_full,
buffer->head,
count);
return;
}
buffer->is_empty = 0;
for (size_t i = 0; i < count; i++) {
buffer->items[buffer->head] = data[i];
advance_pointer(buffer);
}
//me->is_empty = is_full && (me->head == me->tail);
}
int ring_buffer_read(RingBuffer* buffer, float* output, size_t count) {
if (buffer->is_empty) {
write_log("[WARN] Trying to read empty buffer");
return 0;
}
for (size_t i = 0; i < count; i++) {
output[i] = buffer->items[buffer->tail];
retreat_pointer(buffer);
}
buffer->is_empty = !buffer->is_full && (buffer->head == buffer->tail);
return 1;
}
size_t ring_buffer_size(RingBuffer* buffer) {
size_t size = buffer->size;
if(!buffer->is_full) {
if(buffer->head >= buffer->tail) {
size = (buffer->head - buffer->tail);
}
else {
size = (buffer->size + buffer->head - buffer->tail);
}
}
return size;
}
void ring_buffer_print(RingBuffer* me) {
write_log("[INFO] The ring buffer: \n\tIsFull:%d\n\tIsEmpty:%d\n\tHead:%zu\n\tTail:%zu\n\t",
me->is_full,
me->is_empty,
me->head,
me->tail);
}
//------------------------------------------------------------------------------------
// General SynthSound
//------------------------------------------------------------------------------------
typedef struct SynthSound {
float* samples;
size_t sample_count;
} Sound;
} SynthSound;
// frees the original sounds
Sound concat_sounds(Sound* sounds, size_t count) {
SynthSound concat_sounds(SynthSound* sounds, size_t count) {
size_t total_count = 0;
for (size_t i = 0; i < count; i++) {
total_count += sounds[i].sample_count;
@@ -41,7 +163,7 @@ Sound concat_sounds(Sound* sounds, size_t count) {
free(sounds[i].samples);
}
Sound result = {
SynthSound result = {
.samples = total,
.sample_count = total_count
};
@@ -75,7 +197,7 @@ typedef struct OscillatorGenerationParameter {
float sample;
} OscillatorGenerationParameter;
static Sound get_init_samples(float duration) {
static SynthSound get_init_samples(float duration) {
size_t sample_count = (size_t)(duration * SAMPLE_RATE);
float* samples = malloc(sizeof(float) * sample_count);
@@ -83,7 +205,7 @@ static Sound get_init_samples(float duration) {
samples[(int)i] = i;
}
Sound res = {
SynthSound res = {
.samples = samples,
.sample_count = sample_count
};
@@ -96,7 +218,7 @@ static float pos(float hz, float x) {
}
float sineosc(float hz, float x) {
return sinf(x * (2.f * PI * hz / SAMPLE_RATE));
return sinf(x * (2.f * SYNTH_PI * hz / SAMPLE_RATE));
}
static float sign(float v) {
@@ -138,9 +260,9 @@ float multiosc(OscillatorGenerationParameter param) {
return osc_sample;
}
static Sound freq(float duration, OscillatorParameterList osc) {
Sound samples = get_init_samples(duration);
// Sound attack = get_attack_samples();
static SynthSound freq(float duration, OscillatorParameterList osc) {
SynthSound samples = get_init_samples(duration);
// SynthSound attack = get_attack_samples();
float* output = malloc(sizeof(float) * samples.sample_count);
for (int i = 0; i < samples.sample_count; i++) {
@@ -178,8 +300,22 @@ static Sound freq(float duration, OscillatorParameterList osc) {
}
*/
// if (samples.sample_count > 1024) {
// samples.sample_count = 1024;
// }
// //todo: move to somewhere
// for (size_t i = 0; i < 1024; i++) {
// synth_sound.samples[i] = 0.0f;
// }
// for (size_t i = 0; i < samples.sample_count; i++) {
// synth_sound.samples[i] = output[i];
// }
// synth_sound.sample_count = samples.sample_count;
// return zipped array
Sound res = {
SynthSound res = {
.samples = output,
.sample_count = samples.sample_count
};
@@ -188,7 +324,7 @@ static Sound freq(float duration, OscillatorParameterList osc) {
}
/*
static Sound get_attack_samples() {
static SynthSound get_attack_samples() {
float attack_time = 0.001 * ATTACK_MS;
size_t sample_count = (size_t)(attack_time * SAMPLE_RATE);
float* attack = malloc(sizeof(float) * sample_count);
@@ -201,7 +337,7 @@ static Sound get_attack_samples() {
attack[j] = fmin(i, 1.0);
}
Sound res = {
SynthSound res = {
.samples = attack,
.sample_count = sample_count
};
@@ -214,7 +350,7 @@ static Sound get_attack_samples() {
// Synth
//------------------------------------------------------------------------------------
static size_t get_semitone_shift_internal(char* root_note, char* target_note) {
static int get_semitone_shift_internal(char* root_note, char* target_note) {
char* pitch_classes[12] =
{ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" };
@@ -258,21 +394,21 @@ static size_t get_semitone_shift_internal(char* root_note, char* target_note) {
(target_pitch_class - root_pitch_class);
}
static float get_hz_by_semitone(size_t semitone) {
static float get_hz_by_semitone(int semitone) {
return PITCH_STANDARD * powf(powf(2.f, (1.f / 12.f)), semitone);
}
size_t get_semitone_shift(char* target_note) {
int get_semitone_shift(char* target_note) {
return get_semitone_shift_internal("A4", target_note);
}
Sound note(size_t semitone, float beats) {
SynthSound note(int semitone, float beats) {
float hz = get_hz_by_semitone(semitone);
float duration = beats * BEAT_DURATION;
OscillatorParameter first = {
.osc = Saw,
.freq = hz/4.f
.osc = Square,
.freq = hz
};
OscillatorParameter second = {
@@ -285,18 +421,18 @@ Sound note(size_t semitone, float beats) {
.freq = hz - 1.f
};
OscillatorParameter oscArray[] = { first, second, third };
OscillatorParameter oscArray[] = { first/*, second, third */};
OscillatorParameterList parameters = {
.array = oscArray,
.count = 3
.count = 1
};
return freq(duration, parameters);
}
Sound get_note_sound(Note input) {
SynthSound get_note_sound(Note input) {
float length = 1.f / input.length;
size_t semitone_shift = get_semitone_shift(input.name);
int semitone_shift = get_semitone_shift(input.name);
return note(semitone_shift, length);
}
@@ -374,23 +510,189 @@ void pack(uint16_t* d, size_t length) {
write_file("output.wav", buffer, fileSize);
}
size_t detect_note_pressed(Note* note) {
size_t is_pressed = 0;
note->length = 8;
if (IsKeyPressed(KEY_A)) {
strcpy(note->name, "A4");
is_pressed = 1;
}
if (IsKeyPressed(KEY_B)) {
strcpy(note->name, "B4");
is_pressed = 1;
}
if (IsKeyPressed(KEY_C)) {
strcpy(note->name, "C4");
is_pressed = 1;
}
if (IsKeyPressed(KEY_D)) {
strcpy(note->name, "D4");
is_pressed = 1;
}
if (IsKeyPressed(KEY_E)) {
strcpy(note->name, "E4");
is_pressed = 1;
}
if (IsKeyPressed(KEY_F)) {
strcpy(note->name, "F4");
is_pressed = 1;
}
if (IsKeyPressed(KEY_G)) {
strcpy(note->name, "G4");
is_pressed = 1;
}
return is_pressed;
}
//------------------------------------------------------------------------------------
// UI
//------------------------------------------------------------------------------------
/*
int get_zero_crossing(SynthSound* sound) {
int zero_crossing_index = 0;
for (size_t i = 1; i < sound->sample_count; i++)
{
if (sound->samples[i] >= 0.0f && sound->samples[i-1] < 0.0f) // zero-crossing
{
zero_crossing_index = i;
break;
}
}
return zero_crossing_index;
}
Vector2* GetVisualSignal(SynthSound* sound, int zero_crossing_index)
{
const int signal_amp = 300;
Vector2* signal_points = malloc(sizeof(Vector2) * STREAM_BUFFER_SIZE);
const float screen_vertical_midpoint = (WINDOW_HEIGHT/2);
for (size_t point_idx = 0; point_idx < sound->sample_count; point_idx++)
{
const int signal_idx = (point_idx + zero_crossing_index) % STREAM_BUFFER_SIZE;
signal_points[point_idx].x = (float)point_idx + WINDOW_WIDTH;
signal_points[point_idx].y = screen_vertical_midpoint + (int)(sound->samples[signal_idx] * 300);
}
return signal_points;
}
*/
//------------------------------------------------------------------------------------
// Main
//------------------------------------------------------------------------------------
int main(int argc, char **argv) {
InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "SeeSynth - v0.1");
SetTargetFPS(60);
Note current_note = {
.length = 1,
.name = malloc(sizeof(char) * 3)
};
SynthSound sound = {
.sample_count = 0
};
int sound_played_count = 0;
float temp_buffer[STREAM_BUFFER_SIZE];
RingBuffer ring_buffer = ring_buffer_init(STREAM_BUFFER_SIZE);
InitAudioDevice();
SetMasterVolume(SYNTH_VOLUME);
SetAudioStreamBufferSizeDefault(STREAM_BUFFER_SIZE);
AudioStream synth_stream = LoadAudioStream(SAMPLE_RATE, sizeof(float) * 8, 1);//float*8
SetAudioStreamVolume(synth_stream, 0.5f);
PlayAudioStream(synth_stream);
// Main game loop
while (!WindowShouldClose()) // Detect window close button or ESC key
{
// Update Audio states
//----------------------------------------------------------------------------------
// Fill ring buffer from current sound
size_t size_for_buffer = 0;
if (!ring_buffer.is_full && sound.sample_count != sound_played_count) {
write_log("[INFO] IsFull:%d Samples:%zu Played:%zu\n",
ring_buffer.is_full,
sound.sample_count,
sound_played_count);
// how many samples need write
size_t size_to_fill = 0;
if ((sound.sample_count - sound_played_count) > ring_buffer.size) {
size_to_fill = ring_buffer.size;
} else {
size_to_fill = sound.sample_count - sound_played_count;
}
write_log("[INFO] SizeToFill:%zu\n", size_to_fill);
for (size_t i = 0; i < size_to_fill; i++) {
temp_buffer[i] = sound.samples[i];
}
ring_buffer_write(&ring_buffer, temp_buffer, size_to_fill);
sound_played_count += size_to_fill;
}
// Play ring-buffered audio
if (IsAudioStreamProcessed(synth_stream) && !ring_buffer.is_empty) {
size_t size_to_read = ring_buffer_size(&ring_buffer);
write_log("Samples to play:%zu \n", size_to_read);
//todo: try to start reading directly from ring buffer, avoiding temp_buffer
ring_buffer_read(&ring_buffer, temp_buffer, size_to_read);
// can try the SetAudioStreamCallback
UpdateAudioStream(synth_stream, temp_buffer, size_to_read);
// can overwrite the ring buffer to avoid that
if (sound.sample_count == sound_played_count) {
ring_buffer_reset(&ring_buffer);
}
}
//----------------------------------------------------------------------------------
// Update On Input
//----------------------------------------------------------------------------------
if (detect_note_pressed(&current_note)) {
sound = get_note_sound(current_note);
sound_played_count = 0;
write_log("Note played: %s\n", current_note.name);
}
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(RAYWHITE);
// int zero_crossing = get_zero_crossing(&sound);
// Vector2* visual_signal = GetVisualSignal(&sound, zero_crossing);
// DrawLineStrip(visual_signal, STREAM_BUFFER_SIZE - zero_crossing, RED);
DrawText("Congrats! You created your first window!", 190, 200, 20, LIGHTGRAY);
DrawFPS(0,0);
EndDrawing();
//----------------------------------------------------------------------------------
}
char* input = "A4-4 A4-4 A4-4 A4-4 A4-2 A4-4 A4-4 A4-4 A4-4 A4-4 A4-2 D5-4 D5-4 D5-4 D5-4 D5-4 D5-4 D5-2 C5-4 C5-4 C5-4 C5-4 C5-4 C5-4 C5-2 G4-2 ";
char* buf = malloc(strlen(input) + 1);
strcpy(buf, input);
NoteArray note_array = parse_notes(buf, strlen(buf));
Sound* sounds = malloc(sizeof(Sound) * note_array.count);
SynthSound* sounds = malloc(sizeof(SynthSound) * note_array.count);
for (size_t i = 0; i < note_array.count; i++) {
Note note = note_array.notes[i];
sounds[i] = get_note_sound(note);
}
Sound song = concat_sounds(sounds, note_array.count);
SynthSound song = concat_sounds(sounds, note_array.count);
uint16_t* song_pcm = malloc(sizeof(uint16_t) * song.sample_count);
for (size_t i = 0; i < song.sample_count; i++) {
song_pcm[i] = toInt16Sample(song.samples[i]);
@@ -398,5 +700,13 @@ int main(int argc, char **argv) {
pack(song_pcm, song.sample_count);
// De-Initialization
//--------------------------------------------------------------------------------------
StopAudioStream(synth_stream);
UnloadAudioStream(synth_stream);
CloseAudioDevice();
CloseWindow(); // Close window and OpenGL context
//--------------------------------------------------------------------------------------
return 0;
}