Nils
2 years ago
commit
4136a6aed7
38 changed files with 9731 additions and 0 deletions
@ -0,0 +1,4 @@ |
|||
*.out |
|||
*.gch |
|||
*.json |
|||
tgvs |
@ -0,0 +1,251 @@ |
|||
?<html> |
|||
<head> |
|||
<title>The Code Project Open License (CPOL)</title> |
|||
<Style> |
|||
BODY, P, TD { font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 10pt } |
|||
H1,H2,H3,H4,H5 { color: #ff9900; font-weight: bold; } |
|||
H1 { font-size: 14pt;color:black } |
|||
H2 { font-size: 13pt; } |
|||
H3 { font-size: 12pt; } |
|||
H4 { font-size: 10pt; color: black; } |
|||
PRE { BACKGROUND-COLOR: #FBEDBB; FONT-FAMILY: "Courier New", Courier, mono; WHITE-SPACE: pre; } |
|||
CODE { COLOR: #990000; FONT-FAMILY: "Courier New", Courier, mono; } |
|||
.SpacedList li { padding: 5px 0px 5px 0px;} |
|||
</style> |
|||
</head> |
|||
<body bgcolor="#FFFFFF" color=#000000> |
|||
|
|||
<h1>The Code Project Open License (CPOL) 1.02</h1> |
|||
<br /> |
|||
|
|||
<center> |
|||
<div style="text-align: left; border: 2px solid #000000; width: 660; background-color: #FFFFD9; padding: 20px;"> |
|||
|
|||
<h2>Preamble</h2> |
|||
<p> |
|||
This License governs Your use of the Work. This License is intended to allow developers |
|||
to use the Source Code and Executable Files provided as part of the Work in any |
|||
application in any form. |
|||
</p> |
|||
<p> |
|||
The main points subject to the terms of the License are:</p> |
|||
<ul> |
|||
<li>Source Code and Executable Files can be used in commercial applications;</li> |
|||
<li>Source Code and Executable Files can be redistributed; and</li> |
|||
<li>Source Code can be modified to create derivative works.</li> |
|||
<li>No claim of suitability, guarantee, or any warranty whatsoever is provided. The software is |
|||
provided "as-is".</li> |
|||
<li>The Article accompanying the Work may not be distributed or republished without the |
|||
Author's consent</li> |
|||
</ul> |
|||
|
|||
<p> |
|||
This License is entered between You, the individual or other entity reading or otherwise |
|||
making use of the Work licensed pursuant to this License and the individual or other |
|||
entity which offers the Work under the terms of this License ("Author").</p> |
|||
|
|||
<h2>License</h2> |
|||
<p> |
|||
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CODE PROJECT OPEN |
|||
LICENSE ("LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE |
|||
LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT |
|||
LAW IS PROHIBITED.</p> |
|||
<p> |
|||
BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HEREIN, YOU ACCEPT AND AGREE TO BE |
|||
BOUND BY THE TERMS OF THIS LICENSE. THE AUTHOR GRANTS YOU THE RIGHTS CONTAINED HEREIN |
|||
IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. IF YOU DO NOT |
|||
AGREE TO ACCEPT AND BE BOUND BY THE TERMS OF THIS LICENSE, YOU CANNOT MAKE ANY |
|||
USE OF THE WORK.</p> |
|||
|
|||
<ol class="SpacedList"> |
|||
<li><strong>Definitions.</strong> |
|||
|
|||
<ol class="SpacedList" style="list-style-type: lower-alpha;"> |
|||
<li><strong>"Articles"</strong> means, collectively, all articles written by Author |
|||
which describes how the Source Code and Executable Files for the Work may be used |
|||
by a user.</li> |
|||
<li><b>"Author"</b> means the individual or entity that offers the Work under the terms |
|||
of this License.<strong></strong></li> |
|||
<li><strong>"Derivative Work"</strong> means a work based upon the Work or upon the |
|||
Work and other pre-existing works.</li> |
|||
<li><b>"Executable Files"</b> refer to the executables, binary files, configuration |
|||
and any required data files included in the Work.</li> |
|||
<li>"<b>Publisher</b>" means the provider of the website, magazine, CD-ROM, DVD or other |
|||
medium from or by which the Work is obtained by You.</li> |
|||
<li><b>"Source Code"</b> refers to the collection of source code and configuration files |
|||
used to create the Executable Files.</li> |
|||
<li><b>"Standard Version"</b> refers to such a Work if it has not been modified, or |
|||
has been modified in accordance with the consent of the Author, such consent being |
|||
in the full discretion of the Author. </li> |
|||
<li><b>"Work"</b> refers to the collection of files distributed by the Publisher, including |
|||
the Source Code, Executable Files, binaries, data files, documentation, whitepapers |
|||
and the Articles. </li> |
|||
<li><b>"You"</b> is you, an individual or entity wishing to use the Work and exercise |
|||
your rights under this License. |
|||
</li> |
|||
</ol> |
|||
</li> |
|||
|
|||
<li><strong>Fair Use/Fair Use Rights.</strong> Nothing in this License is intended to |
|||
reduce, limit, or restrict any rights arising from fair use, fair dealing, first |
|||
sale or other limitations on the exclusive rights of the copyright owner under copyright |
|||
law or other applicable laws. |
|||
</li> |
|||
|
|||
<li><strong>License Grant.</strong> Subject to the terms and conditions of this License, |
|||
the Author hereby grants You a worldwide, royalty-free, non-exclusive, perpetual |
|||
(for the duration of the applicable copyright) license to exercise the rights in |
|||
the Work as stated below: |
|||
|
|||
<ol class="SpacedList" style="list-style-type: lower-alpha;"> |
|||
<li>You may use the standard version of the Source Code or Executable Files in Your |
|||
own applications. </li> |
|||
<li>You may apply bug fixes, portability fixes and other modifications obtained from |
|||
the Public Domain or from the Author. A Work modified in such a way shall still |
|||
be considered the standard version and will be subject to this License.</li> |
|||
<li>You may otherwise modify Your copy of this Work (excluding the Articles) in any |
|||
way to create a Derivative Work, provided that You insert a prominent notice in |
|||
each changed file stating how, when and where You changed that file.</li> |
|||
<li>You may distribute the standard version of the Executable Files and Source Code |
|||
or Derivative Work in aggregate with other (possibly commercial) programs as part |
|||
of a larger (possibly commercial) software distribution. </li> |
|||
<li>The Articles discussing the Work published in any form by the author may not be |
|||
distributed or republished without the Author's consent. The author retains |
|||
copyright to any such Articles. You may use the Executable Files and Source Code |
|||
pursuant to this License but you may not repost or republish or otherwise distribute |
|||
or make available the Articles, without the prior written consent of the Author.</li> |
|||
</ol> |
|||
|
|||
Any subroutines or modules supplied by You and linked into the Source Code or Executable |
|||
Files of this Work shall not be considered part of this Work and will not be subject |
|||
to the terms of this License. |
|||
</li> |
|||
|
|||
<li><strong>Patent License.</strong> Subject to the terms and conditions of this License, |
|||
each Author hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, |
|||
irrevocable (except as stated in this section) patent license to make, have made, use, import, |
|||
and otherwise transfer the Work.</li> |
|||
|
|||
<li><strong>Restrictions.</strong> The license granted in Section 3 above is expressly |
|||
made subject to and limited by the following restrictions: |
|||
|
|||
<ol class="SpacedList" style="list-style-type: lower-alpha;"> |
|||
<li>You agree not to remove any of the original copyright, patent, trademark, and |
|||
attribution notices and associated disclaimers that may appear in the Source Code |
|||
or Executable Files. </li> |
|||
<li>You agree not to advertise or in any way imply that this Work is a product of Your |
|||
own. </li> |
|||
<li>The name of the Author may not be used to endorse or promote products derived from |
|||
the Work without the prior written consent of the Author.</li> |
|||
<li>You agree not to sell, lease, or rent any part of the Work. This does not restrict |
|||
you from including the Work or any part of the Work inside a larger software |
|||
distribution that itself is being sold. The Work by itself, though, cannot be sold, |
|||
leased or rented.</li> |
|||
<li>You may distribute the Executable Files and Source Code only under the terms of |
|||
this License, and You must include a copy of, or the Uniform Resource Identifier |
|||
for, this License with every copy of the Executable Files or Source Code You distribute |
|||
and ensure that anyone receiving such Executable Files and Source Code agrees that |
|||
the terms of this License apply to such Executable Files and/or Source Code. You |
|||
may not offer or impose any terms on the Work that alter or restrict the terms of |
|||
this License or the recipients' exercise of the rights granted hereunder. You |
|||
may not sublicense the Work. You must keep intact all notices that refer to this |
|||
License and to the disclaimer of warranties. You may not distribute the Executable |
|||
Files or Source Code with any technological measures that control access or use |
|||
of the Work in a manner inconsistent with the terms of this License. </li> |
|||
<li>You agree not to use the Work for illegal, immoral or improper purposes, or on pages |
|||
containing illegal, immoral or improper material. The Work is subject to applicable |
|||
export laws. You agree to comply with all such laws and regulations that may apply |
|||
to the Work after Your receipt of the Work. |
|||
</li> |
|||
</ol> |
|||
</li> |
|||
|
|||
<li><strong>Representations, Warranties and Disclaimer.</strong> THIS WORK IS PROVIDED |
|||
"AS IS", "WHERE IS" AND "AS AVAILABLE", WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES |
|||
OR CONDITIONS OR GUARANTEES. YOU, THE USER, ASSUME ALL RISK IN ITS USE, INCLUDING |
|||
COPYRIGHT INFRINGEMENT, PATENT INFRINGEMENT, SUITABILITY, ETC. AUTHOR EXPRESSLY |
|||
DISCLAIMS ALL EXPRESS, IMPLIED OR STATUTORY WARRANTIES OR CONDITIONS, INCLUDING |
|||
WITHOUT LIMITATION, WARRANTIES OR CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY |
|||
OR FITNESS FOR A PARTICULAR PURPOSE, OR ANY WARRANTY OF TITLE OR NON-INFRINGEMENT, |
|||
OR THAT THE WORK (OR ANY PORTION THEREOF) IS CORRECT, USEFUL, BUG-FREE OR FREE OF |
|||
VIRUSES. YOU MUST PASS THIS DISCLAIMER ON WHENEVER YOU DISTRIBUTE THE WORK OR DERIVATIVE |
|||
WORKS. |
|||
</li> |
|||
|
|||
<li><b>Indemnity. </b>You agree to defend, indemnify and hold harmless the Author and |
|||
the Publisher from and against any claims, suits, losses, damages, liabilities, |
|||
costs, and expenses (including reasonable legal or attorneys’ fees) resulting from |
|||
or relating to any use of the Work by You. |
|||
</li> |
|||
|
|||
<li><strong>Limitation on Liability.</strong> EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE |
|||
LAW, IN NO EVENT WILL THE AUTHOR OR THE PUBLISHER BE LIABLE TO YOU ON ANY LEGAL |
|||
THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES |
|||
ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK OR OTHERWISE, EVEN IF THE AUTHOR |
|||
OR THE PUBLISHER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. |
|||
</li> |
|||
|
|||
<li><strong>Termination.</strong> |
|||
|
|||
<ol style="list-style-type: lower-alpha;"> |
|||
<li>This License and the rights granted hereunder will terminate automatically upon |
|||
any breach by You of any term of this License. Individuals or entities who have |
|||
received Derivative Works from You under this License, however, will not have their |
|||
licenses terminated provided such individuals or entities remain in full compliance |
|||
with those licenses. Sections 1, 2, 6, 7, 8, 9, 10 and 11 will survive any termination |
|||
of this License. </li> |
|||
|
|||
<li>If You bring a copyright, trademark, patent or any other infringement claim against |
|||
any contributor over infringements You claim are made by the Work, your License |
|||
from such contributor to the Work ends automatically.</li> |
|||
|
|||
<li>Subject to the above terms and conditions, this License is perpetual (for the duration |
|||
of the applicable copyright in the Work). Notwithstanding the above, the Author |
|||
reserves the right to release the Work under different license terms or to stop |
|||
distributing the Work at any time; provided, however that any such election will |
|||
not serve to withdraw this License (or any other license that has been, or is required |
|||
to be, granted under the terms of this License), and this License will continue |
|||
in full force and effect unless terminated as stated above. |
|||
</li> |
|||
</ol> |
|||
</li> |
|||
|
|||
<li><strong>Publisher</strong>. The parties hereby confirm that the Publisher shall |
|||
not, under any circumstances, be responsible for and shall not have any liability |
|||
in respect of the subject matter of this License. The Publisher makes no warranty |
|||
whatsoever in connection with the Work and shall not be liable to You or any party |
|||
on any legal theory for any damages whatsoever, including without limitation any |
|||
general, special, incidental or consequential damages arising in connection to this |
|||
license. The Publisher reserves the right to cease making the Work available to |
|||
You at any time without notice</li> |
|||
|
|||
<li><strong>Miscellaneous</strong> |
|||
|
|||
<ol class="SpacedList" style="list-style-type: lower-alpha;"> |
|||
<li>This License shall be governed by the laws of the location of the head office of |
|||
the Author or if the Author is an individual, the laws of location of the principal |
|||
place of residence of the Author.</li> |
|||
<li>If any provision of this License is invalid or unenforceable under applicable law, |
|||
it shall not affect the validity or enforceability of the remainder of the terms |
|||
of this License, and without further action by the parties to this License, such |
|||
provision shall be reformed to the minimum extent necessary to make such provision |
|||
valid and enforceable. </li> |
|||
<li>No term or provision of this License shall be deemed waived and no breach consented |
|||
to unless such waiver or consent shall be in writing and signed by the party to |
|||
be charged with such waiver or consent. </li> |
|||
<li>This License constitutes the entire agreement between the parties with respect to |
|||
the Work licensed herein. There are no understandings, agreements or representations |
|||
with respect to the Work not specified herein. The Author shall not be bound by |
|||
any additional provisions that may appear in any communication from You. This License |
|||
may not be modified without the mutual written agreement of the Author and You. |
|||
</li> |
|||
</ol> |
|||
|
|||
</li> |
|||
</ol> |
|||
|
|||
</div> |
|||
</center> |
|||
|
|||
</body> |
|||
</html> |
@ -0,0 +1,10 @@ |
|||
|
|||
.PHONY: clean |
|||
|
|||
all: |
|||
gcc -o tgvs main.c draw.c gui.c jackclient.c drawhelper.c draw_xpitches_yports.c draw_xports_ypitches.c draw_port_grids.c draw_meterbridge.c draw_activity.c programstate.c jRead.c jWrite.c camera.c effect_starfield.c -lraylib -lGL -lm -lpthread -ldl -lrt -lX11 -ljack -llo |
|||
|
|||
|
|||
clean: |
|||
rm tgvs |
|||
rm *.gch |
@ -0,0 +1,94 @@ |
|||
//Standard lib
|
|||
|
|||
//Third party
|
|||
#include "raylib.h" |
|||
|
|||
//Our own files
|
|||
#include "constants.h" |
|||
#include "programstate.h" |
|||
#include "camera.h" |
|||
|
|||
extern ProgramState programState; |
|||
|
|||
|
|||
// https://www.raylib.com/examples/web/core/loader.html?name=core_2d_camera
|
|||
//Camera controls, such as zoom and reset, are in gui.c
|
|||
|
|||
static int screenWidth; |
|||
static int screenHeight; |
|||
|
|||
void cameraMoveUpdate() { |
|||
programState.camera.offset = (Vector2){ screenWidth/2+programState.cameraCenterOnX, screenHeight/2+programState.cameraCenterOnY }; |
|||
} |
|||
|
|||
void cameraLeft() { |
|||
programState.cameraCenterOnX -= 10; |
|||
cameraMoveUpdate(); |
|||
} |
|||
void cameraRight() { |
|||
programState.cameraCenterOnX += 10; |
|||
cameraMoveUpdate(); |
|||
} |
|||
void cameraUp() { |
|||
programState.cameraCenterOnY -= 10; |
|||
cameraMoveUpdate(); |
|||
} |
|||
void cameraDown() { |
|||
programState.cameraCenterOnY += 10; |
|||
cameraMoveUpdate(); |
|||
} |
|||
|
|||
void cameraMaybeZoom() { |
|||
//float rememberZoom = programState.camera.zoom;
|
|||
if (!programState.lockInput) { |
|||
programState.camera.zoom += ((float)GetMouseWheelMove()*0.05f); |
|||
if (programState.camera.zoom < 0) |
|||
programState.camera.zoom = 0.0f; |
|||
//if (rememberZoom != programState.camera.zoom) {}
|
|||
} |
|||
} |
|||
|
|||
void cameraReset() { |
|||
//Only called through user action. Camera startup values are either loaded from file or set in programstate.c
|
|||
programState.camera.zoom = 1.0f; |
|||
programState.camera.rotation = 0.0f; |
|||
programState.cameraCenterOnX = 0; |
|||
programState.cameraCenterOnY = 0; |
|||
cameraMoveUpdate(); |
|||
} |
|||
|
|||
void cameraNormalizeRotation() { |
|||
if (programState.camera.rotation > 359) programState.camera.rotation -= 360; |
|||
else if (programState.camera.rotation < 0) programState.camera.rotation += 360; |
|||
} |
|||
|
|||
void cameraRotateLeft() { |
|||
programState.camera.rotation--; |
|||
cameraNormalizeRotation(); |
|||
} |
|||
|
|||
void cameraRotateRight() { |
|||
programState.camera.rotation++; |
|||
cameraNormalizeRotation(); |
|||
} |
|||
|
|||
void cameraRotate180() { |
|||
programState.camera.rotation += 180.0f; |
|||
cameraNormalizeRotation(); |
|||
} |
|||
|
|||
void cameraFlip() { |
|||
programState.camera.zoom *= -1.0f; //this is the same as rotation by 180, and not mirror
|
|||
//programState.camera.rotation += 180.0f;
|
|||
} |
|||
|
|||
void mainLoop_cameraReposition() { |
|||
if (programState.guiRedrawNeeded) { |
|||
screenWidth = GetScreenWidth(); //Changes on resize
|
|||
screenHeight = GetScreenHeight(); //Changes on resize
|
|||
programState.camera.target = (Vector2){ screenWidth/2, screenHeight/2 }; |
|||
programState.camera.offset = (Vector2){ screenWidth/2+programState.cameraCenterOnX, screenHeight/2+programState.cameraCenterOnY }; |
|||
} |
|||
} |
|||
|
|||
|
@ -0,0 +1,18 @@ |
|||
#ifndef CAMERA_H |
|||
#define CAMERA_H |
|||
|
|||
void cameraReset(); |
|||
void cameraMaybeZoom(); |
|||
void mainLoop_cameraReposition(); |
|||
void cameraRotateLeft(); |
|||
void cameraRotateRight(); |
|||
void cameraRotate180(); |
|||
//void cameraFlip();
|
|||
|
|||
void cameraLeft(); |
|||
void cameraRight(); |
|||
void cameraUp(); |
|||
void cameraDown(); |
|||
|
|||
|
|||
#endif // not defined CAMERA_H
|
@ -0,0 +1,52 @@ |
|||
#ifndef CONSTANTS_H |
|||
#define CONSTANTS_H |
|||
|
|||
#include "raylib.h" |
|||
|
|||
#ifndef MAX |
|||
#define MAX(a,b) ( (a) < (b) ? (b) : (a) ) |
|||
#endif |
|||
|
|||
#define VIS_PORTS 16 // Program is able to handle <100 ports. But we never intend anything different than 16 to be honest.
|
|||
#define VIS_PORTS_SQRT 4 //please choose by hand. If not possibe choose the next higher. e.g. 13 ports -> 4²
|
|||
|
|||
//Pixel values
|
|||
#define NOTE_SMALL_SIDE 14 |
|||
#define NOTE_LONG_SIDE 20 |
|||
#define NOTE_BORDER 4 //halved for left/right padding. the NOTE sides already include the border.
|
|||
#define NOTE_GAP 3 |
|||
//For GUI
|
|||
#define HEIGHT 25 |
|||
#define SPACING 35 |
|||
|
|||
|
|||
typedef struct { // C-N00b: using typedef lets us type Note nt; or (Note){...} instead of struct Note.
|
|||
int active; |
|||
int port; |
|||
int x; |
|||
int y; |
|||
int pitch; |
|||
int velocity; //converted to color, size etc. by raylib
|
|||
float countdown; //0.0f is not existent, 1 is permanently visible, everything between is the fade out process with auto-decrement. To start the timer set to 0.999 or so
|
|||
} Note; |
|||
|
|||
|
|||
enum { |
|||
//ports are 0-15
|
|||
background=VIS_PORTS+0, |
|||
backgroundLight=VIS_PORTS+1, |
|||
backgroundLighter=VIS_PORTS+2, |
|||
}; |
|||
|
|||
|
|||
enum { |
|||
//Layers in order.
|
|||
layerIndex_notes, |
|||
layerIndex_effects, |
|||
layerIndex_sprites, |
|||
layerIndex_count, //last one
|
|||
}; |
|||
|
|||
#endif // not defined CONSTANTS_H
|
|||
|
|||
|
@ -0,0 +1,144 @@ |
|||
//Standard lib
|
|||
#include <stdio.h> |
|||
#include <stdlib.h> |
|||
#include <string.h> |
|||
#include <unistd.h> |
|||
|
|||
//Third party
|
|||
#include "raylib.h" |
|||
|
|||
//Our own files
|
|||
#include "constants.h" |
|||
#include "programstate.h" |
|||
#include "drawhelper.h" |
|||
#include "draw.h" |
|||
|
|||
//Drawing Routines
|
|||
#include "draw_xpitches_yports.h" |
|||
#include "draw_xports_ypitches.h" |
|||
#include "draw_port_grids.h" |
|||
#include "draw_meterbridge.h" |
|||
#include "draw_activity.h" |
|||
|
|||
//Effect Routines
|
|||
#include "effect_starfield.h" |
|||
|
|||
#define GLSL_VERSION 330 |
|||
|
|||
|
|||
extern ProgramState programState; |
|||
|
|||
Effect effects[NR_OF_EFFECTS] = { |
|||
{ false, "Starfield", false, 1.0f, 1, 2, effect_starfield}, |
|||
{ false, "Snow Fall", false, 1.0f, 1, 3, effect_starfield}, |
|||
{ false, "Falling Leafs", false, 1.0f, 1, 4, effect_starfield}, |
|||
{ false, "Rain", false, 1.0f, 1, 5, effect_starfield}, |
|||
{ false, "Embers", false, 1.0f, 1, 5, effect_starfield}, |
|||
}; |
|||
|
|||
//This needs to be manually synced to gui.c combox
|
|||
//This order is permanent. No re-ordering after the first release!
|
|||
//The save file and the GUI depend on it.
|
|||
void (*drawFunctions[5])() = { |
|||
draw_xpitches_yports, |
|||
draw_xports_ypitches, |
|||
draw_port_grids, |
|||
draw_meterbridge, |
|||
draw_activity, |
|||
}; |
|||
|
|||
|
|||
static Texture2D backgroundTexture; |
|||
static RenderTexture2D renderTarget; |
|||
static Shader shader; |
|||
|
|||
void loadBackgroundImage(char * path, bool newImage) { |
|||
if (programState.nsm) { |
|||
char * temporaryAbsolutePath = malloc (sizeof(char) * 4096); //4096 is the max path length in linux
|
|||
snprintf(temporaryAbsolutePath, (sizeof(char) * 4096), "%s/%s", programState.nsmDirectory, "background.png"); |
|||
if (newImage) { //User clicked on the GUI button and chose a file
|
|||
unlink(temporaryAbsolutePath); |
|||
symlink(path, temporaryAbsolutePath); //source-file, link
|
|||
} |
|||
programState.pathBackgroundImage = "background.png"; |
|||
backgroundTexture = LoadTexture(temporaryAbsolutePath); |
|||
free(temporaryAbsolutePath); |
|||
} |
|||
else { |
|||
programState.pathBackgroundImage = path; |
|||
backgroundTexture = LoadTexture(programState.pathBackgroundImage); |
|||
} |
|||
|
|||
} |
|||
|
|||
void init_draw() { |
|||
renderTarget = LoadRenderTexture(GetScreenWidth(), GetScreenHeight()); |
|||
shader = LoadShader(0, FormatText("glsl%i/bloom.fs", GLSL_VERSION)); //vs file, fs file
|
|||
} |
|||
|
|||
|
|||
void mainLoop_draw(Note *allGUINotes) { |
|||
if (IsWindowResized()) { |
|||
//printf("Window resized to: %i x %i\n", GetScreenWidth(), GetScreenHeight());
|
|||
programState.guiRedrawNeeded = true; |
|||
} |
|||
|
|||
//Drawing Layers from bottom to top:
|
|||
//Background Image (or plain color)
|
|||
//Background Sprites (internal Z-Order = Y)
|
|||
//Background Effects (e.g. Starfield)
|
|||
//Main Visualization (Notes)
|
|||
//Foreground Sprites (internal Z-Order = Y)
|
|||
//Foreground Effects (e.g. Rain)
|
|||
|
|||
|
|||
|
|||
|
|||
if (programState.showBackgroundImageLayer) { |
|||
//void DrawTexture(Texture2D texture, int posX, int posY, Color tint);
|
|||
//Draw centered but not scaled.
|
|||
DrawTexture(backgroundTexture, GetScreenWidth()/2-backgroundTexture.width/2, GetScreenHeight()/2-backgroundTexture.height/2, WHITE); |
|||
} |
|||
|
|||
//BeginTextureMode(renderTarget); // Enable drawing to texture
|
|||
//ClearBackground(BLACK); //Texture background
|
|||
|
|||
//Background Effects Pass
|
|||
if (programState.showEffectLayer) { |
|||
for (int i=0; i<NR_OF_EFFECTS; i++) { |
|||
if (effects[i].enabled && !effects[i].foreground) { |
|||
effects[i].function(programState.guiRedrawNeeded, effects[i].opacity, effects[i].speed, effects[i].size); |
|||
} |
|||
} |
|||
} |
|||
|
|||
//EndTextureMode(); // End drawing to texture (now we have a texture available for next passes)
|
|||
//BeginShaderMode(shader);
|
|||
// DrawTextureRec(renderTarget.texture, (Rectangle){ 0, 0, renderTarget.texture.width, -renderTarget.texture.height }, (Vector2){ 0, 0 }, WHITE);
|
|||
//EndShaderMode();
|
|||
|
|||
|
|||
if (programState.showDrawMode) { |
|||
BeginMode2D(programState.camera); |
|||
drawFunctions[programState.drawMode](allGUINotes, programState.guiRedrawNeeded); //One Mode at a time
|
|||
EndMode2D(); |
|||
} |
|||
|
|||
|
|||
|
|||
//Foreground Effects Pass
|
|||
if (programState.showEffectLayer) { |
|||
for (int i=0; i<NR_OF_EFFECTS; i++) { |
|||
if (effects[i].enabled && effects[i].foreground) { |
|||
effects[i].function(programState.guiRedrawNeeded, effects[i].opacity, effects[i].speed, effects[i].size); |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
//DrawCircle(GetScreenWidth()/2, GetScreenHeight()/2, 10, RAYWHITE); //show center point
|
|||
|
|||
programState.guiRedrawNeeded = false; |
|||
} |
@ -0,0 +1,36 @@ |
|||
#ifndef DRAW_H |
|||
#define DRAW_H |
|||
|
|||
#define NR_OF_EFFECTS 5 |
|||
|
|||
typedef void (*effectFunction)(bool guiRedrawNeeded, float opacity, int speedModifier, int size); |
|||
typedef struct { |
|||
bool enabled; |
|||
char * name; |
|||
bool foreground; |
|||
float opacity; |
|||
int speed; |
|||
int size; // effect depends on the drawing mode. This could be everything
|
|||
effectFunction function; |
|||
} Effect; |
|||
|
|||
typedef struct { |
|||
bool enabled; |
|||
char * path; //NSM = subpath, Standalone: Absolute Path
|
|||
bool foreground; |
|||
float opacity; |
|||
Color tint; |
|||
int x; |
|||
int y; |
|||
float rotation; |
|||
float zoom; |
|||
//If you want animation or different versions you have to put them all in one image and then devide into tiles:
|
|||
int tileSize; //from the whole picture, how big is the square that should be displayed.
|
|||
int tileIndex; //When the image is divided through size which of the resulting tiles should be displayed. 1D, starting from 0, max is 127
|
|||
} Sprite; |
|||
|
|||
void mainLoop_draw(Note *allGUINotes); |
|||
void loadBackgroundImage(char * path, bool newImage); |
|||
void init_draw(); |
|||
|
|||
#endif // not defined DRAW_H
|
@ -0,0 +1,99 @@ |
|||
//Standard lib
|
|||
#include <stdio.h> |
|||
#include <math.h> |
|||
|
|||
//Third party
|
|||
#include "raylib.h" |
|||
|
|||
//Our own files
|
|||
#include "constants.h" |
|||
#include "programstate.h" |
|||
#include "drawhelper.h" |
|||
#include "draw_activity.h" |
|||
|
|||
|
|||
static int screenWidth; |
|||
static int screenHeight; |
|||
|
|||
static int radius = 80; |
|||
const float distanceFactor = 2.5; |
|||
static int xOffset; |
|||
static int yOffset; |
|||
|
|||
extern ProgramState programState; |
|||
|
|||
static float fadeout[VIS_PORTS]; |
|||
|
|||
|
|||
static void reposition(Note *allGUINotes) { |
|||
//This is both init and update. We only call it internally, the name of the function doesn't matter.
|
|||
//Once guaranteed called on program start and then each window resize
|
|||
screenWidth = GetScreenWidth(); |
|||
screenHeight = GetScreenHeight(); |
|||
|
|||
xOffset = (int)(screenWidth/2) - (VIS_PORTS_SQRT/2)*radius*distanceFactor; |
|||
yOffset = (int)(screenHeight/2) - (VIS_PORTS_SQRT/2)*radius*distanceFactor; |
|||
|
|||
programState.modeDescription = "Each port is one polygon.\nNumber of sites equals active notes.\nThere is no time-dimension."; |
|||
} |
|||
|
|||
|
|||
void drawShape(Vector2 center, int port, float rotation) { |
|||
//DrawPolyLines(Vector2 center, int sides, float radius, float rotation, Color color);
|
|||
|
|||
//float fadeValue = pow(sinf(rotation / 180 * PI), 2); //sine wants radians, not degree
|
|||
//float fadeValue = (float)(GetRandomValue(1,10)-5.0f) / 100.0f + 1.0f; //sparkle
|
|||
//Color color = Fade(programState.colors[port], fadeValue);
|
|||
|
|||
Color border = Fade(BLACK, fadeout[port]); |
|||
Color color = Fade(programState.colors[port], fadeout[port]); |
|||
|
|||
if (programState.portActivity[port] < 3) { |
|||
//DrawCircleV(Vector2 center, float radius, Color color);
|
|||
DrawCircleV(center, radius*0.7+NOTE_BORDER, border); |
|||
DrawCircleV(center, radius*0.7, color); |
|||
} |
|||
else { |
|||
DrawPoly(center, programState.portActivity[port], radius+NOTE_BORDER*2, rotation, border); |
|||
DrawPoly(center, programState.portActivity[port], radius, rotation, color); |
|||
} |
|||
} |
|||
|
|||
void draw_activity(Note *allGUINotes, int redrawNeeded) { |
|||
float ft = GetFrameTime(); |
|||
int bpmFactor = programState.bpm/40 * programState.bpm/20; //exponential curve. The higher bpm the faster we get. Below 90 it should feel really slow
|
|||
float rotation = fmod(GetTime() * bpmFactor * 5, 360.0f); |
|||
|
|||
if (redrawNeeded) { |
|||
reposition(allGUINotes); |
|||
} |
|||
|
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
|
|||
int portRow = port / VIS_PORTS_SQRT; |
|||
int portColumn = port % VIS_PORTS_SQRT; |
|||
int blockY = portRow * radius * distanceFactor; |
|||
int blockX = portColumn * radius * distanceFactor; |
|||
Vector2 center = (Vector2){ xOffset + blockX + radius, yOffset + blockY + radius }; |
|||
|
|||
if (programState.showPortBackground && portShouldDraw(port)) { |
|||
DrawCircleV(center, radius-NOTE_BORDER, programState.colors[backgroundLight]); |
|||
} |
|||
|
|||
if (programState.portActivity[port] > 0) { |
|||
//printf("port %i notes %i\n", port, programState.portActivity[port]);
|
|||
fadeout[port] = 1.0f; |
|||
drawShape(center, port, rotation); |
|||
|
|||
} |
|||
else if (fadeout[port] > 0.0f) { |
|||
if (fadeout[port] == 1.0f) { fadeout[port] = 0.999; } //begin countdown
|
|||
fadeout[port] = calculateCountdown(fadeout[port], ft); |
|||
drawShape(center, port, rotation); |
|||
} |
|||
|
|||
if (programState.showConnectedPortnames) { |
|||
drawJackPortName(programState.connectedPortNames[port], xOffset+blockX, yOffset+blockY, 18, RAYWHITE, false); //text, x, y, int fontsize, color, vertical
|
|||
} |
|||
} |
|||
} |
@ -0,0 +1,6 @@ |
|||
#ifndef DRAW_ACTIVITY_H |
|||
#define DRAW_ACTIVITY_H |
|||
|
|||
void draw_activity(Note *allGUINotes, int redrawNeeded); |
|||
|
|||
#endif // not defined DRAW_ACTIVITY_H
|
@ -0,0 +1,115 @@ |
|||
//Standard lib
|
|||
#include <stdio.h> |
|||
|
|||
//Third party
|
|||
#include "raylib.h" |
|||
#include "raygui.h" // Required for GUI controls |
|||
|
|||
//Our own files
|
|||
#include "constants.h" |
|||
#include "programstate.h" |
|||
#include "drawhelper.h" |
|||
#include "draw_meterbridge.h" |
|||
|
|||
extern ProgramState programState; |
|||
static Rectangle backgroundRect; |
|||
static bool guiGroupEditMode= false; |
|||
static int screenHeight; |
|||
static int bottom; |
|||
|
|||
static void reposition(Note *allGUINotes) { |
|||
//This is both init and update. We only call it internally, the name of the function doesn't matter.
|
|||
//Once guaranteed called on program start and then each window resize
|
|||
int screenWidth = GetScreenWidth(); |
|||
int screenHeight = GetScreenHeight(); |
|||
int xOffset = (int)(screenWidth/2) - (int)(60*(NOTE_LONG_SIDE+NOTE_GAP)); //we want all tracks around the center.
|
|||
int yOffset = (int)(screenHeight/2) - (int)(NOTE_SMALL_SIDE+NOTE_GAP)*2; //we want all tracks below the center.
|
|||
|
|||
programState.modeDescription = "All port are in one horizontal row.\
|
|||
\nPitches are left to right, low to high.\ |
|||
\n\ |
|||
\nVelocity 'shoots' notes upwards, then they fall back\ |
|||
\ninfluenced by the fade-out setting.\ |
|||
\n\ |
|||
\nYou can set the a 'grouping' parameter for this mode\ |
|||
\nwhich combines several pitches into a single indicator.\ |
|||
\nGrouping uses the pitch marker pitch as baseline.\ |
|||
\nThink of it like the tonic or root note.\ |
|||
\n\ |
|||
\n\ |
|||
\nNotes are only lit up when they are played.\ |
|||
\nThere is no time-dimension."; |
|||
|
|||
int rootNote = programState.pitchMarkerValue % 12; //factor out the octave. used to start groups on the pitchmarker as root note of the scale
|
|||
int xpos = xOffset; |
|||
int pitchToTheCenter = 64 - (128/programState.meterbridge_grouping/2); |
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
for (int midiPitch=0; midiPitch<128; midiPitch++) { //We prepare the full range of pitches, but the main loop will only activate the current pitch range.
|
|||
int adjustedNotePitch = (int)((midiPitch-rootNote)/programState.meterbridge_grouping) + pitchToTheCenter; |
|||
xpos = xOffset + adjustedNotePitch*NOTE_LONG_SIDE + adjustedNotePitch*NOTE_GAP/2; |
|||
allGUINotes[port*128 + midiPitch].x = xpos; |
|||
allGUINotes[port*128 + midiPitch].y = yOffset + NOTE_SMALL_SIDE + NOTE_GAP*2; |
|||
} |
|||
} |
|||
|
|||
backgroundRect.x = allGUINotes[programState.pitchMin].x; //includes offset
|
|||
backgroundRect.y = (float)(yOffset + NOTE_SMALL_SIDE + NOTE_GAP*2 - NOTE_GAP); |
|||
backgroundRect.width = allGUINotes[programState.pitchMax].x - allGUINotes[programState.pitchMin].x; |
|||
backgroundRect.height = (float)NOTE_SMALL_SIDE; |
|||
} |
|||
|
|||
static void drawBackground() { |
|||
if (programState.showPortBackground) { |
|||
DrawRectangleRec(backgroundRect, programState.colors[backgroundLight]); |
|||
} |
|||
} |
|||
|
|||
void draw_meterbridge(Note *allGUINotes, int redrawNeeded) { |
|||
Note * nt; |
|||
float ft = GetFrameTime(); |
|||
|
|||
if (redrawNeeded) { |
|||
reposition(allGUINotes); |
|||
} |
|||
|
|||
//Z-Order is created by call-order. First is bottom.
|
|||
drawBackground(); |
|||
|
|||
if (programState.showPitchMarker) { |
|||
nt = &allGUINotes[programState.pitchMarkerValue]; |
|||
Rectangle rec = (Rectangle){nt->x, nt->y+NOTE_SMALL_SIDE, NOTE_LONG_SIDE-NOTE_BORDER, NOTE_SMALL_SIDE-NOTE_BORDER}; //TODO: Ha! :) This falls down as well, together with the note.
|
|||
DrawRectangleRounded(rec, 0.5, 8, programState.colors[backgroundLighter]); |
|||
} |
|||
|
|||
//Debug: Show 0 and 127
|
|||
//DrawLine(allGUINotes[0].x, 0, allGUINotes[0].x, GetScreenHeight(), RAYWHITE); //(int startPosX, int startPosY, int endPosX, int endPosY, Color color);
|
|||
//DrawLine(allGUINotes[127].x, 0, allGUINotes[127].x, GetScreenHeight(), RAYWHITE); //(int startPosX, int startPosY, int endPosX, int endPosY, Color color);
|
|||
|
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
for (int midiPitch=programState.pitchMin; midiPitch<=programState.pitchMax; midiPitch++) { |
|||
nt = &allGUINotes[port*128 + midiPitch]; |
|||
if (nt->active) { |
|||
nt->y = backgroundRect.y - 2*(nt->countdown * nt->velocity); //Shift the notes upwards. Don't use -= or +=, that is exponential modifications
|
|||
reduceCountdown(nt, ft/2); //handles 0.0 and 1.0 as special cases
|
|||
drawNoteRect(nt, 1, 0); //note, rotated, square. Will not draw if not active/countdown == 0
|
|||
} |
|||
} |
|||
} |
|||
|
|||
//Our own GUI section
|
|||
if (programState.guiVisible) { |
|||
EndMode2D(); //We are already in camera mode because this is a draw_ function. GUI is not under camera control.
|
|||
int row = 0; |
|||
screenHeight = GetScreenHeight(); //Changes on resize
|
|||
bottom = screenHeight - 2*SPACING; |
|||
int remember = programState.meterbridge_grouping; |
|||
GuiLabel((Rectangle){ 34, bottom-row*SPACING, 105, HEIGHT }, "Meterbridge Note Grouping"); // void GuiLabel(Rectangle bounds, const char *text);
|
|||
if (GuiSpinner((Rectangle){ 250, bottom-row*SPACING, 80, HEIGHT }, "", &programState.meterbridge_grouping, 0, 127, guiGroupEditMode)) guiGroupEditMode = !guiGroupEditMode; // bool GuiSpinner(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode)
|
|||
if (programState.meterbridge_grouping > 128) {programState.meterbridge_grouping=128;} |
|||
if (programState.meterbridge_grouping < 1) {programState.meterbridge_grouping=1;} |
|||
if (remember != programState.meterbridge_grouping) { reposition(allGUINotes); } //we cannot set guiRedrawNeeded to true here because parent draw() will reset it after we return
|
|||
|
|||
row++; |
|||
BeginMode2D(programState.camera); //Switch back to previously active camera mode
|
|||
} |
|||
} |
@ -0,0 +1,6 @@ |
|||
#ifndef DRAW_METERBRIDGE_H |
|||
#define DRAW_METERBRIDGE_H |
|||
|
|||
void draw_meterbridge(Note *allGUINotes, int redrawNeeded); |
|||
|
|||
#endif // not defined DRAW_METERBRIDGE_H
|
@ -0,0 +1,121 @@ |
|||
//Standard lib
|
|||
#include <stdio.h> |
|||
|
|||
//Third party
|
|||
#include "raylib.h" |
|||
|
|||
//Our own files
|
|||
#include "constants.h" |
|||
#include "programstate.h" |
|||
#include "drawhelper.h" |
|||
#include "draw_port_grids.h" |
|||
|
|||
/* Each port is a grid square, ordered in rows, like text
|
|||
* Pitches are put on a 12x12 square for octaves. |
|||
* The screen does not move or scroll. Notes fade out and make room for the next note-on. |
|||
*/ |
|||
|
|||
extern ProgramState programState; |
|||
|
|||
static Rectangle backgroundRects[VIS_PORTS]; // 4 floats: x, y, w, h
|
|||
|
|||
static void reposition(Note *allGUINotes) { |
|||
//This is both init and update. We only call it internally, the name of the function doesn't matter.
|
|||
//Once guaranteed called on program start and then each window resize
|
|||
int screenWidth = GetScreenWidth(); |
|||
int screenHeight = GetScreenHeight(); |
|||
int blockSize = 13 * (NOTE_SMALL_SIDE + NOTE_GAP); //space the blocks one row/column apart = 13
|
|||
int xOffset = (int)(screenWidth/2) - (VIS_PORTS_SQRT/2)*blockSize; //shift half of the grid blocks from the center to the left
|
|||
int yOffset = (int)(screenHeight/2) - (VIS_PORTS_SQRT/2)*blockSize; //shift half of the grid blocks from the center to the left
|
|||
|
|||
//top left corner of each block
|
|||
int blockX; |
|||
int blockY; |
|||
int portRow; |
|||
int portColumn; |
|||
int pitchRow; |
|||
int pitchColumn; |
|||
|
|||
programState.modeDescription = "Each port is one grid.\
|
|||
\nEach pitch is one square, from top left\nto bottom right.\ |
|||
\nPitch rows wrap, just like normal text.\n\n\ |
|||
\n\ |
|||
The higher the velocity of a note the more\nvibrant its color.\n\ |
|||
\n\ |
|||
Notes are only lit up when they are played.\n\ |
|||
There is no time-dimension."; |
|||
|
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
//We loop in a way that is guaranteed to have >= grid blocks available for all notes
|
|||
//But we will never try to access more notes than exist in allGUINotes.
|
|||
portRow = port / VIS_PORTS_SQRT; |
|||
portColumn = port % VIS_PORTS_SQRT; |
|||
blockY = portRow * blockSize; |
|||
blockX = portColumn * blockSize; |
|||
|
|||
backgroundRects[port].x = (float)(xOffset + blockX); |
|||
backgroundRects[port].y = (float)(yOffset + blockY); |
|||
backgroundRects[port].width = (float)(12 * (NOTE_SMALL_SIDE + NOTE_GAP)); //blocksize without filling the extra gap
|
|||
backgroundRects[port].height = (float)(12 * (NOTE_SMALL_SIDE + NOTE_GAP)); |
|||
//Build a 12x12 Grid for octaves.
|
|||
for (int midiPitch=0; midiPitch<128; midiPitch++) { |
|||
pitchRow = midiPitch / 12 +1; //shift one down because it is prettier
|
|||
pitchColumn = midiPitch % 12; |
|||
allGUINotes[port*128 + midiPitch].x = xOffset + blockX + pitchColumn * (NOTE_SMALL_SIDE + NOTE_GAP); |
|||
allGUINotes[port*128 + midiPitch].y = yOffset + blockY + pitchRow * (NOTE_SMALL_SIDE + NOTE_GAP); |
|||
} |
|||
} |
|||
} |
|||
|
|||
static void drawBackground() { |
|||
//Each port is one track
|
|||
//The background rects already have their positions and dimensions set.
|
|||
if (programState.showPortBackground) { |
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
if (portShouldDraw(port)) { |
|||
DrawRectangleRec(backgroundRects[port], programState.colors[backgroundLight]); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
void draw_port_grids(Note *allGUINotes, int redrawNeeded) { |
|||
Note * nt; |
|||
float ft = GetFrameTime(); |
|||
|
|||
if (redrawNeeded) { |
|||
reposition(allGUINotes); |
|||
} |
|||
|
|||
//Z-Order is created by call-order. First is bottom.
|
|||
drawBackground(); |
|||
|
|||
if (programState.showConnectedPortnames) { |
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
nt = &allGUINotes[port*128]; // top left note of each port
|
|||
drawJackPortName(programState.connectedPortNames[port], nt->x, nt->y-1.5*(NOTE_SMALL_SIDE + NOTE_GAP), 18, RAYWHITE, false); //text, x, y, int fontsize, color, vertical
|
|||
} |
|||
} |
|||
|
|||
if (programState.showPitchMarker) { |
|||
//Each block has a pitch marker at one note that we draw with a different color.
|
|||
//We use the already set note positions but create a background note behind the real one.
|
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
if (portShouldDraw(port)) { |
|||
nt = &allGUINotes[port*128 + programState.pitchMarkerValue]; |
|||
Rectangle rec = (Rectangle){nt->x, nt->y, NOTE_SMALL_SIDE-NOTE_BORDER, NOTE_SMALL_SIDE-NOTE_BORDER}; |
|||
DrawRectangleRounded(rec, 0.5, 8, programState.colors[backgroundLighter]); |
|||
} |
|||
} |
|||
} |
|||
|
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
for (int midiPitch=0; midiPitch<128; midiPitch++) { //128 is a fixed midi value, no need to dynamically get the size
|
|||
nt = &allGUINotes[port*128 + midiPitch]; |
|||
if (nt->active) { |
|||
reduceCountdown(nt, ft); //handles 0.0 and 1.0 as special cases
|
|||
drawNoteRect(nt, 0, 1); //note, rotated, square. Will not draw if not active/countdown == 0
|
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,6 @@ |
|||
#ifndef DRAW_PORT_GRIDS_H |
|||
#define DRAW_PORT_GRIDS_H |
|||
|
|||
void draw_port_grids(Note *allGUINotes, int redrawNeeded); |
|||
|
|||
#endif // not defined DRAW_PORT_GRIDS_H
|
@ -0,0 +1,107 @@ |
|||
//Standard lib
|
|||
#include <stdio.h> |
|||
|
|||
//Third party
|
|||
#include "raylib.h" |
|||
|
|||
//Our own files
|
|||
#include "constants.h" |
|||
#include "programstate.h" |
|||
#include "drawhelper.h" |
|||
#include "draw_xpitches_yports.h" |
|||
|
|||
/* Each port is a track, which are ordered top to bottom
|
|||
* Each pitch is a rectangle in that track. midi note 0 on the left to 127 on the right. |
|||
* The screen does not move or scroll. Notes fade out and make room for the next note-on. |
|||
*/ |
|||
|
|||
extern ProgramState programState; |
|||
|
|||
static Rectangle backgroundRects[VIS_PORTS]; // 4 floats: x, y, w, h
|
|||
|
|||
static void reposition(Note *allGUINotes) { |
|||
//This is both init and update. We only call it internally, the name of the function doesn't matter.
|
|||
//Once guaranteed called on program start and then each window resize
|
|||
int screenWidth = GetScreenWidth(); |
|||
int screenHeight = GetScreenHeight(); |
|||
int xOffset = (int)(screenWidth/2) - (int)(60*(NOTE_SMALL_SIDE+NOTE_GAP)); //we want all tracks around the center.
|
|||
int yOffset = (int)(screenHeight/2) - (int)(VIS_PORTS*(NOTE_LONG_SIDE+NOTE_GAP))/2; //we want all tracks around the center.
|
|||
|
|||
programState.modeDescription = "Each port is a horizontal track.\n\
|
|||
Each pitch is one square, going from left lowest\nto right highest.\n\ |
|||
\n\ |
|||
The higher the velocity of a note the more\nvibrant its color.\n\ |
|||
\n\ |
|||
Notes are only lit up when they are played.\n\ |
|||
There is no time-dimension."; |
|||
|
|||
|
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
backgroundRects[port].x = xOffset + programState.pitchMin * (NOTE_SMALL_SIDE + NOTE_GAP/2); |
|||
backgroundRects[port].y = (float)(yOffset + port*NOTE_LONG_SIDE + port*NOTE_GAP*2 - NOTE_GAP); |
|||
backgroundRects[port].width = (programState.pitchMax-programState.pitchMin+1)*(NOTE_SMALL_SIDE + NOTE_GAP/2); |
|||
backgroundRects[port].height = (float)NOTE_LONG_SIDE; |
|||
|
|||
for (int midiPitch=0; midiPitch<128; midiPitch++) { //We prepare the full range of pitches, but the main loop will only activate the current pitch range.
|
|||
allGUINotes[port*128 + midiPitch].x = xOffset + midiPitch*(NOTE_SMALL_SIDE + NOTE_GAP/2); |
|||
allGUINotes[port*128 + midiPitch].y = yOffset + port*NOTE_LONG_SIDE + port*NOTE_GAP*2; |
|||
} |
|||
} |
|||
} |
|||
|
|||
static void drawBackground() { |
|||
//Each port is one track
|
|||
//The background rects already have their positions and dimensions set.
|
|||
|
|||
if (programState.showPortBackground) { |
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
if (portShouldDraw(port)) |
|||
{ |
|||
DrawRectangleRec(backgroundRects[port], programState.colors[backgroundLight]); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
void draw_xpitches_yports(Note *allGUINotes, int redrawNeeded) { |
|||
Note * nt; |
|||
float ft = GetFrameTime(); |
|||
|
|||
if (redrawNeeded) { |
|||
reposition(allGUINotes); |
|||
} |
|||
|
|||
//Z-Order is created by call-order. First is bottom.
|
|||
drawBackground(); |
|||
|
|||
if (programState.showConnectedPortnames) { |
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
nt = &allGUINotes[port*128+programState.pitchMin]; // choose note where to start writing
|
|||
drawJackPortName(programState.connectedPortNames[port], nt->x, nt->y, 18, RAYWHITE, false); //text, x, y, int fontsize, color, vertical
|
|||
} |
|||
} |
|||
|
|||
if (programState.showPitchMarker) { |
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
if (portShouldDraw(port)) { |
|||
nt = &allGUINotes[port*128 + programState.pitchMarkerValue]; |
|||
Rectangle rec = (Rectangle){nt->x, nt->y, NOTE_SMALL_SIDE-NOTE_BORDER, NOTE_LONG_SIDE-NOTE_BORDER}; |
|||
DrawRectangleRounded(rec, 0.5, 8, programState.colors[backgroundLighter]); |
|||
} |
|||
} |
|||
} |
|||
|
|||
//Debug: Show 0 and 127
|
|||
//DrawLine(allGUINotes[0].x, 0, allGUINotes[0].x, GetScreenHeight(), RAYWHITE); //(int startPosX, int startPosY, int endPosX, int endPosY, Color color);
|
|||
//DrawLine(allGUINotes[127].x, 0, allGUINotes[127].x, GetScreenHeight(), RAYWHITE); //(int startPosX, int startPosY, int endPosX, int endPosY, Color color);
|
|||
|
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
for (int midiPitch=programState.pitchMin; midiPitch<=programState.pitchMax; midiPitch++) { |
|||
nt = &allGUINotes[port*128 + midiPitch]; |
|||
if (nt->active) { |
|||
reduceCountdown(nt, ft); //handles 0.0 and 1.0 as special cases
|
|||
drawNoteRect(nt, 0, 0); //note, rotated, square. Will not draw if countdown == 0
|
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,6 @@ |
|||
#ifndef DRAW_XPITCHES_YPORTS_H |
|||
#define DRAW_XPITCHES_YPORTS_H |
|||
|
|||
void draw_xpitches_yports(Note *allGUINotes, int redrawNeeded); |
|||
|
|||
#endif // not defined DRAW_XPITCHES_YPORTS_H
|
@ -0,0 +1,103 @@ |
|||
//Standard lib
|
|||
|
|||
//Third party
|
|||
#include "raylib.h" |
|||
|
|||
//Our own files
|
|||
#include "constants.h" |
|||
#include "programstate.h" |
|||
#include "drawhelper.h" |
|||
#include "draw_xports_ypitches.h" |
|||
|
|||
/* Each port is a column, ordered from left to right
|
|||
* Each pitch is a rectangle in that column. midi note 0 on the bottom to 127 on top |
|||
* The screen does not move or scroll. Notes fade out and make room for the next note-on. |
|||
*/ |
|||
|
|||
extern ProgramState programState; |
|||
|
|||
static Rectangle backgroundRects[VIS_PORTS]; // 4 floats: x, y, w, h
|
|||
|
|||
static void reposition(Note *allGUINotes) { |
|||
//This is both init and update. We only call it internally, the name of the function doesn't matter.
|
|||
//Once guaranteed called on program start and then each window resize
|
|||
int screenWidth = GetScreenWidth(); |
|||
int screenHeight = GetScreenHeight(); |
|||
int xOffset = (int)(screenWidth/2) - (int)((VIS_PORTS*NOTE_LONG_SIDE)/2); |
|||
//int yOffset = -1 * ((int)(screenWidth/2) - ((programState.pitchMax - programState.pitchMin +1 ) * NOTE_SMALL_SIDE))/2;
|
|||
int yOffset = -1 * ((int)(screenHeight/2) - (60 * NOTE_SMALL_SIDE)/2); //we want all tracks around the center.
|
|||
|
|||
programState.modeDescription = "Each port is a vertical column.\n\
|
|||
Each pitch is one square, going from top highest\nto bottomw lowest.\n\ |
|||
\n\ |
|||
The higher the velocity of a note the more\nvibrant its color.\n\ |
|||
\n\ |
|||
Notes are only lit up when they are played.\n\ |
|||
There is no time-dimension."; |
|||
|
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
backgroundRects[port].x = xOffset + port*(NOTE_LONG_SIDE + NOTE_GAP) - NOTE_GAP/2; |
|||
backgroundRects[port].y = yOffset + (127-programState.pitchMax) * NOTE_SMALL_SIDE; |
|||
backgroundRects[port].width = (float)NOTE_LONG_SIDE; |
|||
backgroundRects[port].height = (programState.pitchMax - programState.pitchMin +1 ) * NOTE_SMALL_SIDE; |
|||
|
|||
for (int midiPitch=0; midiPitch<128; midiPitch++) { //We prepare the full range of pitches, but the main loop will only activate the current pitch range.
|
|||
allGUINotes[port*128 + midiPitch].x = xOffset + port*(NOTE_LONG_SIDE + NOTE_GAP); |
|||
allGUINotes[port*128 + midiPitch].y = yOffset + (127-midiPitch) * NOTE_SMALL_SIDE ; |
|||
} |
|||
} |
|||
} |
|||
|
|||
static void drawBackground() { |
|||
//Each port is one track
|
|||
//The background rects already have their positions and dimensions set.
|
|||
if (programState.showPortBackground) { |
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
if (portShouldDraw(port)) { |
|||
DrawRectangleRec(backgroundRects[port], programState.colors[backgroundLight]); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
void draw_xports_ypitches(Note *allGUINotes, int redrawNeeded) { |
|||
Note * nt; |
|||
float ft = GetFrameTime(); |
|||
|
|||
if (redrawNeeded) { |
|||
reposition(allGUINotes); |
|||
} |
|||
|
|||
//Z-Order is created by call-order. First is bottom.
|
|||
drawBackground(); |
|||
|
|||
|
|||
if (programState.showConnectedPortnames) { |
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
nt = &allGUINotes[port*128+programState.pitchMin]; // bottom left note of each port
|
|||
drawJackPortName(programState.connectedPortNames[port], nt->x, nt->y+20, 18, RAYWHITE, true); //text, x, y, int fontsize, color, vertical
|
|||
} |
|||
} |
|||
|
|||
if (programState.showPitchMarker) { |
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
if (portShouldDraw(port)) { |
|||
nt = &allGUINotes[port*128 + programState.pitchMarkerValue]; |
|||
Rectangle rec = (Rectangle){nt->x, nt->y, NOTE_LONG_SIDE-NOTE_BORDER, NOTE_SMALL_SIDE-NOTE_BORDER}; |
|||
DrawRectangleRounded(rec, 0.5, 8, programState.colors[backgroundLighter]); |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
for (int midiPitch=programState.pitchMin; midiPitch<=programState.pitchMax; midiPitch++) { |
|||
nt = &allGUINotes[port*128 + midiPitch]; |
|||
if (nt->active) { |
|||
reduceCountdown(nt, ft); //handles 0.0 and 1.0 as special cases
|
|||
drawNoteRect(nt, 1, 0); //note, rotated, square. Will not draw if countdown == 0
|
|||
} |
|||
|
|||
} |
|||
} |
|||
} |
@ -0,0 +1,6 @@ |
|||
#ifndef DRAW_XPORTS_YPITCHES_H |
|||
#define DRAW_XPORTS_YPITCHES_H |
|||
|
|||
void draw_xports_ypitches(Note *allGUINotes, int redrawNeeded); |
|||
|
|||
#endif // not defined DRAW_XPORTS_YPITCHES_H
|
@ -0,0 +1,181 @@ |
|||
//Standard lib
|
|||
#include <string.h> //strchr |
|||
#include <stdio.h> //print error messages |
|||
|
|||
//Third party
|
|||
#include "raylib.h" |
|||
|
|||
//Our own files
|
|||
#include "constants.h" |
|||
#include "programstate.h" |
|||
#include "drawhelper.h" |
|||
|
|||
extern ProgramState programState; |
|||
|
|||
|
|||
float intToHue(int number, int maxnumber) { |
|||
//previously used to auto-color ports this is now only used for the default port colors.
|
|||
|
|||
//portnum is 0-VIS_PORTS
|
|||
//hue is 0-360
|
|||
//return (input-inputLowest) / (inputHighest-inputLowest) * (outputHighest-outputLowest) + outputLowest
|
|||
return (float)number / (float)maxnumber * 360.0f; |
|||
} |
|||
|
|||
Color intToColor(int number, int maxnumber) { |
|||
Color result; |
|||
Vector3 hsv; // 3 floats: hue/color, saturation, value/brightness
|
|||
|
|||
hsv = (Vector3){intToHue(number, VIS_PORTS), 1.0f, (float)(127+40)/(float)167}; //Saturation is always 1, we tweak Value to never go into the black range.
|
|||
result = ColorFromHSV(hsv); |
|||
return result; |
|||
} |
|||
|
|||
Color midiToColor(Note * note) { |
|||
Color result; |
|||
Vector3 hsv; // 3 floats: hue/color, saturation, value/brightness
|
|||
|
|||
//hsv = (Vector3){intToHue(note->port, VIS_PORTS), 1.0f, (float)(note->velocity+40)/(float)167}; //Saturation is always 1, we tweak Value to never go into the black range.
|
|||
float hue=ColorToHSV(programState.colors[note->port]).x; |
|||
hsv = (Vector3){hue, 1.0f, (float)(note->velocity+40)/(float)167}; //Saturation is always 1, we tweak Value to never go into the black range.
|
|||
result = ColorFromHSV(hsv); |
|||
return result; |
|||
} |
|||
|
|||
void drawNoteRect(Note * note, int rotated, int square) { |
|||
if (note->active) { |
|||
//opacity is between 0.0f and 1.0f
|
|||
int w,h; |
|||
|
|||
if (square==1) { |
|||
w = NOTE_SMALL_SIDE - NOTE_BORDER; |
|||
h = NOTE_SMALL_SIDE - NOTE_BORDER; |
|||
} |
|||
else { //Not Square
|
|||
if (rotated==0) { |
|||
w = NOTE_SMALL_SIDE - NOTE_BORDER; |
|||
h = NOTE_LONG_SIDE - NOTE_BORDER; |
|||
} |
|||
else { |
|||
h = NOTE_SMALL_SIDE - NOTE_BORDER; |
|||
w = NOTE_LONG_SIDE - NOTE_BORDER; |
|||
} |
|||
} |
|||
|
|||
const Rectangle rec = (Rectangle){note->x, note->y, w,h}; |
|||
const Rectangle outer = (Rectangle){note->x-NOTE_BORDER/2, note->y-NOTE_BORDER/2, w+NOTE_BORDER, h+NOTE_BORDER}; |
|||
|
|||
Color bordercolor = Fade(BLACK, note->countdown); |
|||
Color color = Fade(programState.colors[note->port], note->countdown); |
|||
|
|||
//Drawing order is reverse Z-Order
|
|||
DrawRectangleRounded(outer, 0.5, 8, bordercolor); //float roundness, int segments/smoothness, Color
|
|||
DrawRectangleRounded(rec, 0.5, 8, color); |
|||
} |
|||
} |
|||
|
|||
bool portShouldDraw(int port) { |
|||
//Check various conditions if a port background, port name and pitch marker should be drawn
|
|||
if ( programState.showPortBackgroundForUnconnected || ( programState.connectedPortNames[port] && programState.connectedPortNames[port][0] != '\0' )) { |
|||
return true; |
|||
} |
|||
else { |
|||
return false; |
|||
} |
|||
|
|||
} |
|||
|
|||
void drawJackPortName(const char * portname, int x, int y, int fontsize, Color color, bool vertical) { |
|||
|
|||
if (portname && portname[0] != '\0') { |
|||
|
|||
const char * finalPortName; |
|||
if (programState.includeClientInPortNameDisplay) { |
|||
finalPortName = portname; //TODO: Pretty Names don't contain the client name. Add it ourselves?
|
|||
} |
|||
else { |
|||
if (strchr(portname, ':') == NULL ) //Pretty Names don't contain the client name.
|
|||
{ |
|||
finalPortName = portname; |
|||
} |
|||
else { |
|||
finalPortName = strchr(portname, ':')+1; |
|||
} |
|||
} //final port name is ready.
|
|||
|
|||
|
|||
//Vertical Mode
|
|||
//void DrawTextCodepoint(Font font, int codepoint, Vector2 position, float scale, Color tint); // Draw one character (codepoint)
|
|||
if (vertical) { |
|||
//TODO: Wow, that is ugly!
|
|||
int counter = 0; |
|||
char ch = finalPortName[counter]; |
|||
Font defaultFont = GetFontDefault(); |
|||
while (ch != '\0') { |
|||
DrawTextCodepoint(defaultFont, ch, (Vector2){ x, y+counter*(fontsize/1.5) }, 1.8f, color); |
|||
counter++; |
|||
ch = finalPortName[counter]; |
|||
} |
|||
} |
|||
else { |
|||
// Horizontal Mode
|
|||
DrawText(finalPortName, x, y, fontsize, color); |
|||
} |
|||
} |
|||
} |
|||
|
|||
float calculateCountdown(float countdown, float ft) { |
|||
float bpmFactor; |
|||
|
|||
switch (programState.fadeOutMode) { |
|||
case 0: // framelegnth * user factor
|
|||
if (countdown < 1.0f) { countdown -= (float)programState.fadeOutFactor * ft; } //note off has been triggered. Fade out.
|
|||
if (countdown <= 0.0f) { countdown = 0; } // Is it now, or was already, below 0?
|
|||
break; |
|||
|
|||
case 1: // bpm
|
|||
bpmFactor = (float)(programState.bpm/40); |
|||
if (countdown < 1.0f) { countdown -= bpmFactor * ft; } //note off has been triggered. Fade out.
|
|||
if (countdown <= 0.0f) { countdown = 0.0f; } // Is it now, or was already, below 0?
|
|||
break; |
|||
|
|||
case 2: // no fadeout
|
|||
if (countdown < 1.0f) { countdown = 0.0f; } // As soon as note off arrives
|
|||
break; |
|||
|
|||
default: //just in case
|
|||
countdown = 0.0f; |
|||
break; |
|||
} |
|||
|
|||
return countdown; |
|||
} |
|||
|
|||
void reduceCountdown(Note * nt, float ft) { |
|||
//Notes countdown is set to 0 (note-off), 1 (note-on) or 0.999 (note-off) by our jack midi client.
|
|||
|
|||
float bpmFactor; |
|||
|
|||
if (nt->active == 1) { |
|||
switch (programState.fadeOutMode) { |
|||
case 0: // framelegnth * user factor
|
|||
if (nt->countdown < 1.0f) { nt->countdown -= (float)programState.fadeOutFactor * ft; } //note off has been triggered. Fade out.
|
|||
if (nt->countdown <= 0.0f) { nt->active = false; } // Is it now, or was already, below 0?
|
|||
break; |
|||
|
|||
case 1: // bpm
|
|||
bpmFactor = (float)(programState.bpm/40); |
|||
if (nt->countdown < 1.0f) { nt->countdown -= bpmFactor * ft; } //note off has been triggered. Fade out.
|
|||
if (nt->countdown <= 0.0f) { nt->active = false; } // Is it now, or was already, below 0?
|
|||
break; |
|||
|
|||
case 2: // no fadeout
|
|||
if (nt->countdown < 1.0f) { nt->active = false; } // As soon as note off arrives
|
|||
break; |
|||
|
|||
default: //just in case
|
|||
nt->active = false; |
|||
break; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,14 @@ |
|||
#ifndef DRAWHELPER_H |
|||
#define DRAWHELPER_H |
|||
|
|||
#include "constants.h" |
|||
|
|||
Color midiToColor(Note * note); |
|||
Color intToColor(int number, int maxnumber); |
|||
void drawNoteRect(Note * note, int rotated, int square); |
|||
void reduceCountdown(Note * nt, float ft); |
|||
float calculateCountdown(float countdown, float ft); |
|||
void drawJackPortName(const char * portname, int x, int y, int fontsize, Color color, bool vertical); |
|||
bool portShouldDraw(int port); |
|||
|
|||
#endif // not defined DRAWHELPER_H
|
@ -0,0 +1,100 @@ |
|||
//Standard lib
|
|||
#include <stdio.h> |
|||
|
|||
//Third party
|
|||
#include "raylib.h" |
|||
|
|||
//Our own files
|
|||
#include "constants.h" |
|||
#include "programstate.h" |
|||
#include "effect_starfield.h" |
|||
|
|||
extern ProgramState programState; |
|||
static int screenWidth; |
|||
static int screenHeight; |
|||
static const int numberOfStars=540; |
|||
|
|||
static const int starLayers = 3; |
|||
|
|||
typedef struct { |
|||
int x; |
|||
int y; |
|||
int z; |
|||
Color color; |
|||
float rand; |
|||
} Star; |
|||
|
|||
static Star stars[540]; //1080 * 0.5 Not quite one star per line
|
|||
|
|||
void reposition() { |
|||
//Recreate all stars and begin from blank.
|
|||
//We want a certain star density, no matter the screen resolution
|
|||
|
|||
screenWidth = GetScreenWidth(); |
|||
screenHeight = GetScreenHeight(); |
|||
Color color; |
|||
int rand; |
|||
int layer; |
|||
int partitionSize=(starLayers+1)*2 / 8; //8 partions
|
|||
for (int i=0; i<numberOfStars; i++) { |
|||
rand = GetRandomValue(1, (starLayers+1)*2); |
|||
if (rand<= 5*partitionSize) layer = 1; |
|||
else if (rand<= 7*partitionSize) layer = 2; |
|||
else if (rand<= 8*partitionSize) layer = 3; |
|||
|
|||
switch (layer) { |
|||
//0 does not exist
|
|||
case 1: |
|||
color = BROWN; |
|||
break; |
|||
case 2: |
|||
color = BEIGE; |
|||
break; |
|||
case 3: |
|||
color = PURPLE; |
|||
break; |
|||
default: |
|||
color = DARKBLUE; |
|||
} |
|||
|
|||
|
|||
float randomFactor = ((float)GetRandomValue(0, 20)-10.0f) / 100.0f; |
|||
stars[i] = (Star){ GetRandomValue(0, screenWidth), GetRandomValue(0, screenHeight), layer, color, randomFactor }; |
|||
} |
|||
} |
|||
|
|||
void effect_starfield(bool redrawNeeded, float opacity, int speedModifier, int size) { |
|||
|
|||
float ft = GetFrameTime(); |
|||
int bpmFactor = programState.bpm/40 * programState.bpm/20; //exponential curve. The higher bpm the faster we get. Below 90 it should feel really slow
|
|||
Star * star; |
|||
|
|||
if (redrawNeeded) { |
|||
reposition(); |
|||
} |
|||
|
|||
for (int i=0; i<numberOfStars; i++) { |
|||
star = &stars[i]; |
|||
|
|||
if (programState.transportRolling) { |
|||
//Move Forward
|
|||
star->x -= (star->z * ft * bpmFactor * 3 * speedModifier); //... the mysterious number 3.
|
|||
//wrap around
|
|||
if (star->x <= 0) { |
|||
star->x += screenWidth; |
|||
star->y = GetRandomValue(0, screenHeight); |
|||
} |
|||
else if (star->x > screenWidth) { |
|||
star->x -= screenWidth; |
|||
star->y = GetRandomValue(0, screenHeight); |
|||
} |
|||
} |
|||
|
|||
//Wobble wobble
|
|||
int rand = GetRandomValue(1, 200); |
|||
if (rand == 1) {star->y += 1;} |
|||
else if (rand == 2) {star->y -= 1;} |
|||
|
|||
DrawRectangle(star->x, star->y, size, size, Fade(star->color, opacity)); |
|||
} |
|||
} |
@ -0,0 +1,6 @@ |
|||
#ifndef effect_starfield_H |
|||
#define effect_starfield_H |
|||
|
|||
void effect_starfield(bool redrawNeeded, float opacity, int speedModifier, int size); |
|||
|
|||
#endif // not defined effect_starfield_H
|
Binary file not shown.
@ -0,0 +1,40 @@ |
|||
#version 330 |
|||
|
|||
// Input vertex attributes (from vertex shader) |
|||
in vec2 fragTexCoord; |
|||
in vec4 fragColor; |
|||
|
|||
// Input uniform values |
|||
uniform sampler2D texture0; |
|||
uniform vec4 colDiffuse; |
|||
|
|||
// Output fragment color |
|||
out vec4 finalColor; |
|||
|
|||
// NOTE: Add here your custom variables |
|||
|
|||
const vec2 size = vec2(1920, 1080); // render size |
|||
const float samples = 5.0; // pixels per axis; higher = bigger glow, worse performance |
|||
const float quality = 2.5; // lower = smaller glow, better quality |
|||
|
|||
void main() |
|||
{ |
|||
vec4 sum = vec4(0); |
|||
vec2 sizeFactor = vec2(1)/size*quality; |
|||
|
|||
// Texel color fetching from texture sampler |
|||
vec4 source = texture(texture0, fragTexCoord); |
|||
|
|||
const int range = 2; // should be = (samples - 1)/2; |
|||
|
|||
for (int x = -range; x <= range; x++) |
|||
{ |
|||
for (int y = -range; y <= range; y++) |
|||
{ |
|||
sum += texture(texture0, fragTexCoord + vec2(x, y)*sizeFactor); |
|||
} |
|||
} |
|||
|
|||
// Calculate final fragment color |
|||
finalColor = ((sum/(samples*samples)) + source)*colDiffuse; |
|||
} |
@ -0,0 +1,393 @@ |
|||
//Standard lib
|
|||
#include <stdlib.h> // atoi for commandline argument char to int |
|||
#include <stdio.h> |
|||
#include <string.h> |
|||
#include <unistd.h> |
|||
|
|||
//Third party
|
|||
#include "raylib.h" |
|||
#define RAYGUI_IMPLEMENTATION |
|||
#include "raygui.h" // Required for GUI controls |
|||
|
|||
#define GUI_FILE_DIALOG_IMPLEMENTATION |
|||
#include "gui_file_dialog.h" |
|||
|
|||
|
|||
//Our own files
|
|||
#include "constants.h" |
|||
#include "programstate.h" |
|||
#include "gui.h" |
|||
#include "jackclient.h" |
|||
#include "camera.h" |
|||
#include "draw.h" |
|||
|
|||
extern ProgramState programState; |
|||
extern Effect effects[NR_OF_EFFECTS]; |
|||
|
|||
static int screenWidth; |
|||
static int screenHeight; |
|||
static int center; |
|||
static int bottom; |
|||
static int remember; // Helper var: Did a value change so we need to instruct a gui reposition/draw?
|
|||
static float alpha; //Helper var for colors.
|
|||
static Vector2 panelScroll = { 0, 0 }; //This is the current state of scrolling. It must be declared outside the loop of course
|
|||
|
|||
static int currentColorIndex=0; |
|||
static int currentPaletteIndex=0; //see programstate.c setColorPalette()
|
|||
|
|||
static int columnX; //in pixel
|
|||
static const int checkboxWidth = 24; |
|||
|
|||
static bool colorChoiceEditMode = false; |
|||
static bool colorPaletteEditMode = false; |
|||
static bool pitchMarkerEditMode = false; |
|||
static bool pitchMaxEditMode = false; |
|||
static bool pitchMinEditMode= false; |
|||
|
|||
static int configTab = layerIndex_notes; //index from enum in constants.h
|
|||
|
|||
static char fileNameToLoad[512] = { 0 }; |
|||
static GuiFileDialogState fileDialogState; |
|||
|
|||
//"Port 00;Port 01;Port 02;Port 03;Port 04;Port 05;Port 06;Port 07;Port 08;Port 09;Port 10;Port 11;Port 12;Port 13;Port 14;Port 15;Background;Panels;Marker"
|
|||
static char colorsPortsString[VIS_PORTS*9 + 25]; |
|||
|
|||
void init_gui() { |
|||
for (int i=0; i<VIS_PORTS; i++) { |
|||
//Only works if VIS_PORTS is below 100 ports.
|
|||
snprintf(colorsPortsString+strlen(colorsPortsString), sizeof(colorsPortsString), "Port %2d;", i); //9 because the substring is 9 chars.
|
|||
} |
|||
snprintf(colorsPortsString+strlen(colorsPortsString), sizeof(colorsPortsString), "Background;Panels;Marker"); //Attach to the end
|
|||
|
|||
fileDialogState = InitGuiFileDialog(420, 310, GetWorkingDirectory(), false); |
|||
} |
|||
|
|||
bool guiIsKeyPressed(int keyConstant) { |
|||
//Wrap Raylib function to only accept input when we want it.
|
|||
if (programState.lockInput) { |
|||
return false; |
|||
} |
|||
else { |
|||
return IsKeyPressed(keyConstant); |
|||
} |
|||
} |
|||
bool guiIsKeyDown(int keyConstant) { |
|||
//Wrap Raylib function to only accept input when we want it.
|
|||
if (programState.lockInput) { |
|||
return false; |
|||
} |
|||
else { |
|||
return IsKeyDown(keyConstant); |
|||
} |
|||
} |
|||
|
|||
|
|||
void mainLoop_gui() { |
|||
//bool pressed = IsMouseButtonPressed(0) || IsMouseButtonPressed(1); //0 left, 1 right //Registers no matter where you clicked
|
|||
//if (pressed) { printf("click\n"); };
|
|||
|
|||
GuiUnlock(); |
|||
programState.lockInput = false; |
|||
|
|||
if (fileDialogState.SelectFilePressed) |
|||
{ |
|||
// Load image file (if supported extension)
|
|||
|
|||
if (IsFileExtension(fileDialogState.fileNameText, ".png")) |
|||
{ |
|||
strcpy(fileNameToLoad, TextFormat("%s/%s", fileDialogState.dirPathText, fileDialogState.fileNameText)); |
|||
loadBackgroundImage(fileNameToLoad, true); //also saves the path in programstate
|
|||
} |
|||
|
|||
fileDialogState.SelectFilePressed = false; |
|||
} |
|||
|
|||
GuiFileDialog(&fileDialogState); //does nothing if not activated. This needs to be processed before the GUI locks.
|
|||
|
|||
if (fileDialogState.fileDialogActive) { |
|||
GuiLock(); |
|||
programState.lockInput = true; |
|||
} |
|||
|
|||
//Shortcuts and toggles
|
|||
bool controlDown=false; |
|||
if ( guiIsKeyDown(KEY_LEFT_CONTROL) || guiIsKeyDown(KEY_RIGHT_CONTROL) ) { |
|||
controlDown = true; |
|||
} |
|||
|
|||
if (controlDown && guiIsKeyPressed(KEY_S)) { |
|||
saveStateToFile(); |
|||
} |
|||
|
|||
if (guiIsKeyPressed(KEY_SPACE)){ toggleTransport(); } |
|||
if (guiIsKeyPressed(KEY_G) || guiIsKeyPressed(KEY_ESCAPE)) { programState.guiVisible = !programState.guiVisible; } |
|||
if (guiIsKeyPressed(KEY_B)){ programState.showPortBackground = !programState.showPortBackground; } |
|||
if (guiIsKeyPressed(KEY_P)){ programState.showPitchMarker = !programState.showPitchMarker; } |
|||
if (guiIsKeyPressed(KEY_N)){ programState.showConnectedPortnames = !programState.showConnectedPortnames; } |
|||
if (guiIsKeyPressed(KEY_C)){ programState.alwaysShowClock = !programState.alwaysShowClock; } |
|||
|
|||
//Camera Controls
|
|||
cameraMaybeZoom(); |
|||
if (guiIsKeyPressed(KEY_R)){ cameraReset(); } |
|||
|
|||
if (guiIsKeyDown(KEY_A)) { cameraLeft(); } |
|||
else if (guiIsKeyDown(KEY_D)) { cameraRight(); } |
|||
|
|||
if (guiIsKeyDown(KEY_W)) { cameraUp(); } |
|||
else if (!controlDown && guiIsKeyDown(KEY_S)) { cameraDown(); } |
|||
|
|||
if (guiIsKeyDown(KEY_Q)) { cameraRotateLeft(); } |
|||
else if (guiIsKeyDown(KEY_E)) { cameraRotateRight(); } |
|||
else if (guiIsKeyPressed(KEY_X)) { cameraRotate180(); } |
|||
|
|||
|
|||
//Undocumented because it is based on the current window size.
|
|||
if (guiIsKeyPressed(KEY_F)){ ToggleFullscreen(); } |
|||
|
|||
if (guiIsKeyPressed(KEY_M)){ |
|||
programState.drawMode++; |
|||
if (programState.drawMode > 4) { programState.drawMode = 0;} //wrap around
|
|||
programState.guiRedrawNeeded = true; |
|||
} |
|||
|
|||
//Draw GUI Elements
|
|||
//All position numbers are trial and error
|
|||
//RayGUI draws and delievers the result in one ste. e.g. button clicked.
|
|||
//The state of e.g. ComboBox index is given each time in the caller function.
|
|||
//newState = GuiElement(RectPositionDimension, options, oldState);
|
|||
|
|||
if (programState.alwaysShowClock || programState.guiVisible) { |
|||
//DrawFPS(0,0); //Fugly!
|
|||
screenWidth = GetScreenWidth(); //Changes on resize
|
|||
screenHeight = GetScreenHeight(); //Changes on resize
|
|||
center = screenWidth/2; |
|||
bottom = screenHeight - 2*SPACING; |
|||
//DrawText(programState.clockString, center, bottom-2*SPACING, 20, RAYWHITE); //text, x, y, int fontsize, color
|
|||
DrawTextEx(GuiGetFont(), programState.clockString, (Vector2){center, bottom-2*SPACING}, 31, 1, RAYWHITE); |
|||
} |
|||
|
|||
if (programState.guiVisible) { |
|||
|
|||
//Clock is drawn outside of guiVisible with an individual check
|
|||
|
|||
//Transport Control Buttons
|
|||
if (GuiButton((Rectangle){center-100-5, bottom, 100, HEIGHT}, "Rewind")) { |
|||
//No shortcut
|
|||
seekToStart(); |
|||
} |
|||
if (GuiButton((Rectangle){center, bottom, 205, HEIGHT}, "[Space] Play / Pause")) { |
|||
toggleTransport(); |
|||
} |
|||
|
|||
//Save
|
|||
if (programState.filePath && programState.filePath[0] != '\0') { |
|||
if (GuiButton((Rectangle){ screenWidth-120, bottom-1*SPACING, 105, HEIGHT }, "[Ctrl+S] Save")) { |
|||
saveStateToFile(); |
|||
} |
|||
} |
|||
|
|||
//Quit
|
|||
programState.instructedToQuit = GuiButton((Rectangle){ screenWidth-120, bottom, 105, HEIGHT }, "Quit"); |
|||
|
|||
int row=1; |
|||
columnX = 10; //in pixel
|
|||
|
|||
//Configs for all layers
|
|||
|
|||
//Layer Config "Tabs" Panel
|
|||
DrawRectangle(columnX, row*SPACING-5, 160+230, HEIGHT+10, (Color){ 35, 40, 47, 255 }); |
|||
GuiLabel((Rectangle){ columnX+checkboxWidth, row*SPACING, 105, HEIGHT }, "Show Config for"); // void GuiLabel(Rectangle bounds, const char *text);
|
|||
configTab = GuiComboBox((Rectangle){ 160, row*SPACING, 200, HEIGHT }, "Music Layer;Effects;Sprites;General", configTab); |
|||
row++; |
|||
|
|||
//Tab for each Layer. Begin at row=2
|
|||
if (configTab == layerIndex_notes) { |
|||
|
|||
remember = programState.showDrawMode; |
|||
programState.showDrawMode = GuiCheckBox((Rectangle){ columnX, row*SPACING, 20, HEIGHT }, "Enable Note Layer", programState.showDrawMode); |
|||
if (remember != programState.showDrawMode) { programState.guiRedrawNeeded = true;} |
|||
|
|||
row++; |
|||
|
|||
remember = programState.drawMode; |
|||
GuiLabel((Rectangle){ columnX+checkboxWidth, row*SPACING, 105, HEIGHT }, "[M] Note Layer Mode"); // void GuiLabel(Rectangle bounds, const char *text);
|
|||
programState.drawMode = GuiComboBox((Rectangle){ 200, row*SPACING, 130, HEIGHT }, "Horizontal;Vertical;Grid;Meterbridge;Port Shapes;", programState.drawMode); //These need to be manually synced to draw.c drawFunctions[]
|
|||
if (remember != programState.drawMode) { programState.guiRedrawNeeded = true;} |
|||
|
|||
row++; |
|||
|
|||
//Show Port Background Checkbox
|
|||
programState.showPortBackground = GuiCheckBox((Rectangle){ columnX, row*SPACING, 20, HEIGHT }, "[B] Port Background", programState.showPortBackground); |
|||
|
|||
row++; |
|||
|
|||
//Show Unconnected Port Background Checkbox
|
|||
programState.showPortBackgroundForUnconnected = GuiCheckBox((Rectangle){ columnX, row*SPACING, 20, HEIGHT }, "Draw unconnected Port Background", programState.showPortBackgroundForUnconnected); |
|||
|
|||
row++; |
|||
|
|||
//Pitch Marker
|
|||
programState.showPitchMarker = GuiCheckBox((Rectangle){ columnX, row*SPACING, 20, HEIGHT }, "[P] Pitch Marker", programState.showPitchMarker); |
|||
remember = programState.pitchMarkerValue; |
|||
if (GuiSpinner((Rectangle){ 200, row*SPACING, 80, HEIGHT }, "", &programState.pitchMarkerValue, 0, 127, pitchMarkerEditMode)) pitchMarkerEditMode = !pitchMarkerEditMode; // bool GuiSpinner(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode)
|
|||
//Bug in raygui. If edit mode for spinner is true the max check is not done.
|
|||
if (programState.pitchMarkerValue > 127) {programState.pitchMarkerValue=127;} |
|||
if (programState.pitchMarkerValue < 0) {programState.pitchMarkerValue=0;} |
|||
if (remember != programState.pitchMarkerValue) { programState.guiRedrawNeeded = true;} |
|||
|
|||
row++; |
|||
|
|||
// FadeOut Mode
|
|||
GuiLabel((Rectangle){ columnX+checkboxWidth, row*SPACING, 105, HEIGHT }, "Fade Out Mode"); // void GuiLabel(Rectangle bounds, const char *text);
|
|||
programState.fadeOutMode = GuiComboBox((Rectangle){ 200, row*SPACING, 150, HEIGHT }, "Fixed;Tempo (bpm);Off", programState.fadeOutMode); //This needs to be manually synced to programState options
|
|||
|
|||
if (programState.fadeOutMode == 0) { |
|||
GuiSpinner((Rectangle){ 75+220+120, row*SPACING, 80, HEIGHT }, "Factor", &programState.fadeOutFactor, 1, 9, false); // bool GuiSpinner(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode)
|
|||
//Bug in raygui. If edit mode for spinner is true the max check is not done.
|
|||
if (programState.fadeOutFactor > 9) {programState.fadeOutFactor=9;} |
|||
if (programState.fadeOutFactor < 1) {programState.fadeOutFactor=1;} |
|||
} |
|||
|
|||
row++; |
|||
|
|||
// Show Port Names
|
|||
programState.showConnectedPortnames = GuiCheckBox((Rectangle){ columnX, row*SPACING, 20, HEIGHT }, "[N] Show Connected Port Names", programState.showConnectedPortnames); |
|||
row++; |
|||
programState.includeClientInPortNameDisplay = GuiCheckBox((Rectangle){ columnX, row*SPACING, 20, HEIGHT }, "Include Client in Port Names", programState.includeClientInPortNameDisplay); |
|||
|
|||
row++; |
|||
|
|||
|
|||
|
|||
//Pitch Range Min
|
|||
GuiLabel((Rectangle){ columnX+checkboxWidth, row*SPACING, 105, HEIGHT }, "Pitch Range Minimum"); // void GuiLabel(Rectangle bounds, const char *text);
|
|||
remember = programState.pitchMin; |
|||
if (GuiSpinner((Rectangle){ 200, row*SPACING, 80, HEIGHT }, "", &programState.pitchMin, 0, 127, pitchMinEditMode)) pitchMinEditMode = !pitchMinEditMode; // bool GuiSpinner(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode)
|
|||
//Bug in raygui. If edit mode for spinner is true the max check is not done.
|
|||
if (programState.pitchMin > 127) {programState.pitchMin=127;} |
|||
if (programState.pitchMin < 0) {programState.pitchMin=0;} |
|||
if (remember != programState.pitchMin) { programState.guiRedrawNeeded = true;} |
|||
|
|||
row++; |
|||
|
|||
//Pitch Range Max
|
|||
GuiLabel((Rectangle){ columnX+checkboxWidth, row*SPACING, 105, HEIGHT }, "Pitch Range Maximum"); // void GuiLabel(Rectangle bounds, const char *text);
|
|||
remember = programState.pitchMax; |
|||
if (GuiSpinner((Rectangle){ 200, row*SPACING, 80, HEIGHT }, "", &programState.pitchMax, 0, 127, pitchMaxEditMode)) pitchMaxEditMode = !pitchMaxEditMode; // bool GuiSpinner(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode)
|
|||
//Bug in raygui. If edit mode for spinner is true the max check is not done.
|
|||
if (programState.pitchMax > 127) {programState.pitchMax=127;} |
|||
if (programState.pitchMax < 0) {programState.pitchMax=0;} |
|||
if (remember != programState.pitchMax) { programState.guiRedrawNeeded = true;} |
|||
|
|||
|
|||
//Second Column
|
|||
////////////////
|
|||
columnX=400; |
|||
|
|||
|
|||
//NOTE: GuiDropdownBox must draw after any other control that can be covered on unfolding
|
|||
//Therefore no automatic rows
|
|||
GuiLabel((Rectangle){ columnX, 2*SPACING, 105, HEIGHT }, "Color for"); |
|||
GuiLabel((Rectangle){ columnX, 5*SPACING, 105, HEIGHT }, "Set Palette"); |
|||
remember = currentPaletteIndex; |
|||
if (GuiDropdownBox((Rectangle){ columnX, 5*SPACING, 130, HEIGHT }, "Default;Random;Grayscale", ¤tPaletteIndex, colorPaletteEditMode)) { colorPaletteEditMode = !colorPaletteEditMode; } |
|||
if (remember != currentPaletteIndex) { setColorPalette(currentPaletteIndex);} |
|||
//Draw last in this column
|
|||
if (GuiDropdownBox((Rectangle){ columnX, 3*SPACING, 130, HEIGHT }, colorsPortsString, ¤tColorIndex, colorChoiceEditMode)) colorChoiceEditMode = !colorChoiceEditMode; |
|||
|
|||
//Third Column
|
|||
////////////////
|
|||
row = 2; |
|||
columnX=550; |
|||
|
|||
row++; alpha = GuiColorBarAlpha((Rectangle){ columnX, row*SPACING, 200, HEIGHT }, (float)programState.colors[currentColorIndex].a/255.0f); |
|||
programState.colors[currentColorIndex] = Fade(programState.colors[currentColorIndex], alpha); |
|||
row++; programState.colors[currentColorIndex] = GuiColorPicker((Rectangle){ columnX, row*SPACING, 196, 192 }, programState.colors[currentColorIndex]); |
|||
char colorString[24]; |
|||
sprintf(colorString, "R%i G%i B%i A%i", programState.colors[currentColorIndex].r, programState.colors[currentColorIndex].g, programState.colors[currentColorIndex].b, programState.colors[currentColorIndex].a); |
|||
GuiLabel((Rectangle){ columnX, row*SPACING+190, 105, HEIGHT }, colorString); |
|||
|
|||
|
|||
//Fourth Column
|
|||
////////////////
|
|||
|
|||
row = 2; |
|||
columnX=850; |
|||
|
|||
//Show keys for camera control
|
|||
GuiLabel((Rectangle){ columnX, row*SPACING, 105, HEIGHT }, "Camera Controls"); |
|||
row++; GuiLabel((Rectangle){ columnX, row*SPACING, 105, HEIGHT }, "[W][A][S][D] Pan"); |
|||
row++; GuiLabel((Rectangle){ columnX, row*SPACING, 105, HEIGHT }, "[Q][E][X] Rotate"); |
|||
row++; GuiLabel((Rectangle){ columnX, row*SPACING, 105, HEIGHT }, "[R] Reset"); |
|||
|
|||
row++; |
|||
|
|||
//Help Label 40 per with 80 characters in the top right corner.
|
|||
GuiLabel((Rectangle){ screenWidth-450, 1*SPACING, 105, HEIGHT }, "Mode Help"); // void GuiLabel(Rectangle bounds, const char *text);
|
|||
Rectangle panelRec = { screenWidth-450, 2*SPACING, 410, 300 }; //What the user sees
|
|||
Rectangle panelContentRec = {0, 0, 400, 500 }; //content, bigger than panelRec
|
|||
Rectangle helpText = GuiScrollPanel(panelRec, panelContentRec, &panelScroll); //Rectangle GuiScrollPanel(Rectangle bounds, Rectangle content, Vector2 *scroll);
|
|||
BeginScissorMode(helpText.x, helpText.y, helpText.width, helpText.height); // Begin scissor mode (define screen area for following drawing)
|
|||
//GuiLabelColor((Rectangle){ panelRec.x + panelScroll.x, panelRec.y + panelScroll.y, panelContentRec.width, panelContentRec.height }, "Hallo Leute hier ist\nEin schöner Text für euch. Cool!\nUnd noch ne Zeile.", BLACK); // void GuiLabelColor(Rectangle bounds, const char *text, Color color);
|
|||
GuiLabelColor((Rectangle){ panelRec.x + panelScroll.x, -4.5*SPACING + panelScroll.y, panelContentRec.width, panelContentRec.height }, programState.modeDescription, RAYWHITE); // void GuiLabelColor(Rectangle bounds, const char *text, Color color);
|
|||
EndScissorMode(); |
|||
|
|||
} |
|||
|
|||
//Effect Layer
|
|||
else if (configTab == layerIndex_effects) { |
|||
|
|||
//Show BG Layer Switch
|
|||
remember = programState.showEffectLayer; |
|||
programState.showEffectLayer = GuiCheckBox((Rectangle){ columnX, row*SPACING, 20, HEIGHT }, "Enable Effects", programState.showEffectLayer); |
|||
if (remember != programState.showEffectLayer) { programState.guiRedrawNeeded = true;} |
|||
|
|||
row++; |
|||
|
|||
|
|||
for (int i=0; i<NR_OF_EFFECTS; i++) { |
|||
//Enabled
|
|||
remember =effects[i].enabled; |
|||
effects[i].enabled = GuiCheckBox((Rectangle){ columnX+10, row*SPACING, 20, HEIGHT }, effects[i].name, effects[i].enabled); |
|||
if (remember != effects[i].enabled) { programState.guiRedrawNeeded = true;} |
|||
|
|||
//Foreground
|
|||
effects[i].foreground = GuiCheckBox((Rectangle){ columnX+150, row*SPACING, 20, HEIGHT }, "in foreground", effects[i].foreground); |
|||
|
|||
//Opacity
|
|||
effects[i].opacity = GuiColorBarAlpha((Rectangle){ columnX+300, row*SPACING, 100, HEIGHT }, effects[i].opacity); |
|||
|
|||
//Speed
|
|||
effects[i].speed = GuiSlider((Rectangle){ columnX+460, row*SPACING, 100, HEIGHT }, "Speed", TextFormat("x%i", effects[i].speed), effects[i].speed, -4, 4); |
|||
|
|||
//Size
|
|||
effects[i].size = GuiSlider((Rectangle){ columnX+660, row*SPACING, 100, HEIGHT }, "Size", TextFormat("%i", effects[i].size), effects[i].size, 1, 8); |
|||
|
|||
row++; |
|||
} |
|||
} |
|||
|
|||
//General Config
|
|||
else if (configTab == layerIndex_count) { |
|||
// Show Clock even when GUI is not visible
|
|||
programState.alwaysShowClock = GuiCheckBox((Rectangle){ columnX, row*SPACING, 20, HEIGHT }, "[C] Clock always visible", programState.alwaysShowClock); |
|||
|
|||
row++; |
|||
|
|||
remember = programState.showBackgroundImageLayer; |
|||
programState.showBackgroundImageLayer = GuiCheckBox((Rectangle){ columnX, row*SPACING, 20, HEIGHT }, "Enable Background Image", programState.showBackgroundImageLayer); |
|||
if (remember != programState.showBackgroundImageLayer) { programState.guiRedrawNeeded = true;} |
|||
|
|||
row++; |
|||
|
|||
if (GuiButton((Rectangle){ columnX, row*SPACING, 205, HEIGHT}, "Open Image")) { |
|||
fileDialogState.fileDialogActive = true; |
|||
} |
|||
|
|||
} |
|||
|
|||
|
|||
} |
|||
} |
|||
|
|||
|
@ -0,0 +1,7 @@ |
|||
#ifndef GUI_H |
|||
#define GUI_H |
|||
|
|||
void mainLoop_gui(); |
|||
void init_gui(); |
|||
|
|||
#endif // not defined GUI_H
|
@ -0,0 +1,659 @@ |
|||
/*******************************************************************************************
|
|||
* |
|||
* FileDialog v1.1 - Modal file dialog to open/save files |
|||
* |
|||
* MODULE USAGE: |
|||
* #define GUI_FILE_DIALOG_IMPLEMENTATION |
|||
* #include "gui_file_dialog.h" |
|||
* |
|||
* INIT: GuiFileDialogState state = InitGuiFileDialog(); |
|||
* DRAW: GuiFileDialog(&state); |
|||
* |
|||
* NOTE: This module depends on some raylib file system functions: |
|||
* - GetDirectoryFiles() |
|||
* - ClearDirectoryFiles() |
|||
* - GetWorkingDirectory() |
|||
* - DirectoryExists() |
|||
* - FileExists() |
|||
* |
|||
* LICENSE: zlib/libpng |
|||
* |
|||
* Copyright (c) 2019-2020 Ramon Santamaria (@raysan5) |
|||
* |
|||
* This software is provided "as-is", without any express or implied warranty. In no event |
|||
* will the authors be held liable for any damages arising from the use of this software. |
|||
* |
|||
* Permission is granted to anyone to use this software for any purpose, including commercial |
|||
* applications, and to alter it and redistribute it freely, subject to the following restrictions: |
|||
* |
|||
* 1. The origin of this software must not be misrepresented; you must not claim that you |
|||
* wrote the original software. If you use this software in a product, an acknowledgment |
|||
* in the product documentation would be appreciated but is not required. |
|||
* |
|||
* 2. Altered source versions must be plainly marked as such, and must not be misrepresented |
|||
* as being the original software. |
|||
* |
|||
* 3. This notice may not be removed or altered from any source distribution. |
|||
* |
|||
**********************************************************************************************/ |
|||
|
|||
#include "raylib.h" |
|||
|
|||
// WARNING: raygui implementation is expected to be defined before including this header
|
|||
#undef RAYGUI_IMPLEMENTATION |
|||
#include "raygui.h" |
|||
|
|||
#ifndef GUI_FILE_DIALOG_H |
|||
#define GUI_FILE_DIALOG_H |
|||
|
|||
typedef struct { |
|||
Vector2 position; |
|||
Vector2 size; |
|||
|
|||
bool fileDialogActive; |
|||
|
|||
bool dirPathEditMode; |
|||
char dirPathText[256]; |
|||
|
|||
int filesListScrollIndex; |
|||
bool filesListEditMode; |
|||
int filesListActive; |
|||
|
|||
bool fileNameEditMode; |
|||
char fileNameText[256]; |
|||
bool SelectFilePressed; |
|||
bool CancelFilePressed; |
|||
int fileTypeActive; |
|||
int itemFocused; |
|||
|
|||
// Custom state variables (depend on development software)
|
|||
// NOTE: This variables should be added manually if required
|
|||
char **dirFiles; |
|||
int dirFilesCount; |
|||
|
|||
char filterExt[256]; |
|||
|
|||
char dirPathTextCopy[256]; |
|||
char fileNameTextCopy[256]; |
|||
|
|||
int prevFilesListActive; |
|||
|
|||
} GuiFileDialogState; |
|||
|
|||
#ifdef __cplusplus |
|||
extern "C" { // Prevents name mangling of functions
|
|||
#endif |
|||
|
|||
//----------------------------------------------------------------------------------
|
|||
// Defines and Macros
|
|||
//----------------------------------------------------------------------------------
|
|||
//...
|
|||
|
|||
//----------------------------------------------------------------------------------
|
|||
// Types and Structures Definition
|
|||
//----------------------------------------------------------------------------------
|
|||
// ...
|
|||
|
|||
//----------------------------------------------------------------------------------
|
|||
// Global Variables Definition
|
|||
//----------------------------------------------------------------------------------
|
|||
//...
|
|||
|
|||
//----------------------------------------------------------------------------------
|
|||
// Module Functions Declaration
|
|||
//----------------------------------------------------------------------------------
|
|||
GuiFileDialogState InitGuiFileDialog(int width, int height, const char *initPath, bool active); |
|||
void GuiFileDialog(GuiFileDialogState *state); |
|||
|
|||
#ifdef __cplusplus |
|||
} |
|||
#endif |
|||
|
|||
#endif // GUI_FILE_DIALOG_H
|
|||
|
|||
/***********************************************************************************
|
|||
* |
|||
* GUI_FILE_DIALOG IMPLEMENTATION |
|||
* |
|||
************************************************************************************/ |
|||
#if defined(GUI_FILE_DIALOG_IMPLEMENTATION) |
|||
|
|||
#include "raygui.h" |
|||
|
|||
#include <string.h> // Required for: strcpy() |
|||
|
|||
//----------------------------------------------------------------------------------
|
|||
// Defines and Macros
|
|||
//----------------------------------------------------------------------------------
|
|||
#define MAX_DIRECTORY_FILES 1024 |
|||
#define MAX_DIR_PATH_LENGTH 1024 |
|||
|
|||
//----------------------------------------------------------------------------------
|
|||
// Types and Structures Definition
|
|||
//----------------------------------------------------------------------------------
|
|||
|
|||
#if defined(USE_CUSTOM_LISTVIEW_FILEINFO) |
|||
// Detailed file info type
|
|||
typedef struct FileInfo { |
|||
const char *name; |
|||
int size; |
|||
int modTime; |
|||
int type; |
|||
int icon; |
|||
} FileInfo; |
|||
#else |
|||
// Filename only
|
|||
typedef char *FileInfo; |
|||
#endif |
|||
|
|||
//----------------------------------------------------------------------------------
|
|||
// Global Variables Definition
|
|||
//----------------------------------------------------------------------------------
|
|||
FileInfo *dirFilesIcon = NULL; |
|||
|
|||
//----------------------------------------------------------------------------------
|
|||
// Internal Module Functions Definition
|
|||
//----------------------------------------------------------------------------------
|
|||
// Read all filenames from directory (supported file types)
|
|||
static char **ReadDirectoryFiles(const char *dir, int *filesCount, char *filterExt); |
|||
|
|||
#if defined(USE_CUSTOM_LISTVIEW_FILEINFO) |
|||
// List View control for files info with extended parameters
|
|||
static int GuiListViewFiles(Rectangle bounds, FileInfo *files, int count, int *focus, int *scrollIndex, int active); |
|||
#endif |
|||
|
|||
//----------------------------------------------------------------------------------
|
|||
// Module Functions Definition
|
|||
//----------------------------------------------------------------------------------
|
|||
GuiFileDialogState InitGuiFileDialog(int width, int height, const char *initPath, bool active) |
|||
{ |
|||
GuiFileDialogState state = { 0 }; |
|||
|
|||
// Default dialog size is 440x310
|
|||
state.size.x = width == -1 ? 440 : width; |
|||
state.size.y = height == -1 ? 310 : height; |
|||
state.position = (Vector2){ GetScreenWidth()/2 - state.size.x/2, GetScreenHeight()/2 - state.size.y/2 }; |
|||
|
|||
state.fileDialogActive = active; |
|||
state.dirPathEditMode = false; |
|||
|
|||
state.filesListActive = -1; |
|||
state.prevFilesListActive = state.filesListActive; |
|||
state.filesListScrollIndex = 0; |
|||
|
|||
state.fileNameEditMode = false; |
|||
|
|||
state.SelectFilePressed = false; |
|||
state.CancelFilePressed = false; |
|||
|
|||
state.fileTypeActive = 0; |
|||
|
|||
strcpy(state.fileNameText, "\0"); |
|||
|
|||
// Custom variables initialization
|
|||
if (initPath && DirectoryExists(initPath)) |
|||
{ |
|||
strcpy(state.dirPathText, initPath); |
|||
} |
|||
else if (initPath && FileExists(initPath)) |
|||
{ |
|||
strcpy(state.dirPathText, GetDirectoryPath(initPath)); |
|||
strcpy(state.fileNameText, GetFileName(initPath)); |
|||
} |
|||
else strcpy(state.dirPathText, GetWorkingDirectory()); |
|||
|
|||
strcpy(state.dirPathTextCopy, state.dirPathText); |
|||
strcpy(state.fileNameTextCopy, state.fileNameText); |
|||
|
|||
strcpy(state.filterExt, "all"); |
|||
|
|||
state.dirFilesCount = 0; |
|||
state.dirFiles = NULL; // NOTE: Loaded lazily on window active
|
|||
|
|||
return state; |
|||
} |
|||
|
|||
// Read files in new path
|
|||
static void FD_RELOAD_DIRPATH(GuiFileDialogState *state) |
|||
{ |
|||
for (int i = 0; i < state->dirFilesCount; i++) RL_FREE(state->dirFiles[i]); |
|||
RL_FREE(state->dirFiles); |
|||
|
|||
state->dirFiles = ReadDirectoryFiles(state->dirPathText, &state->dirFilesCount, state->filterExt); |
|||
state->itemFocused = 0; |
|||
} |
|||
|
|||
// Update and draw file dialog
|
|||
void GuiFileDialog(GuiFileDialogState *state) |
|||
{ |
|||
if (state->fileDialogActive) |
|||
{ |
|||
const int winWidth = state->size.x; |
|||
const int winHeight = state->size.y; |
|||
|
|||
// Load dirFilesIcon and state->dirFiles lazily on windows open
|
|||
// NOTE: they are automatically unloaded at fileDialog closing
|
|||
//------------------------------------------------------------------------------------
|
|||
if (dirFilesIcon == NULL) |
|||
{ |
|||
dirFilesIcon = (FileInfo *)RL_MALLOC(MAX_DIRECTORY_FILES*sizeof(FileInfo)); // Max files to read
|
|||
for (int i = 0; i < MAX_DIRECTORY_FILES; i++) dirFilesIcon[i] = (char *)calloc(MAX_DIR_PATH_LENGTH, 1); // Max file name length
|
|||
} |
|||
|
|||
if (state->dirFiles == NULL) |
|||
{ |
|||
state->dirFiles = ReadDirectoryFiles(state->dirPathText, &state->dirFilesCount, state->filterExt); |
|||
|
|||
for(int f = 0; f < state->dirFilesCount; f++) |
|||
{ |
|||
if (strcmp(state->fileNameText, state->dirFiles[f]) == 0) |
|||
{ |
|||
if (state->filesListActive != f) state->filesListScrollIndex = state->filesListActive = f; // make it active and visible only on first call
|
|||
|
|||
break; |
|||
} |
|||
} |
|||
} |
|||
//------------------------------------------------------------------------------------
|
|||
|
|||
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Fade(GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR)), 0.85f)); |
|||
state->fileDialogActive = !GuiWindowBox((Rectangle){ state->position.x + 0, state->position.y + 0, winWidth, winHeight }, "#198#LuaJIT | Select File Dialog"); |
|||
|
|||
if (GuiButton((Rectangle){ state->position.x + winWidth - 50, state->position.y + 35, 40, 25 }, "< .."))// || IsKeyReleased(KEY_DPAD_Y))
|
|||
{ |
|||
// Move dir path one level up
|
|||
strcpy(state->dirPathText, GetPrevDirectoryPath(state->dirPathText)); |
|||
|
|||
// RL_FREE previous dirFiles (reloaded by ReadDirectoryFiles())
|
|||
FD_RELOAD_DIRPATH(state); |
|||
|
|||
state->filesListActive = -1; |
|||
strcpy(state->fileNameText, "\0"); |
|||
strcpy(state->fileNameTextCopy, state->fileNameText); |
|||
} |
|||
|
|||
if (GuiTextBox((Rectangle){ state->position.x + 10, state->position.y + 35, winWidth - 65, 25 }, state->dirPathText, 256, state->dirPathEditMode)) |
|||
{ |
|||
if (state->dirPathEditMode) |
|||
{ |
|||
// Verify if a valid path has been introduced
|
|||
if (DirectoryExists(state->dirPathText)) |
|||
{ |
|||
// RL_FREE previous dirFiles (reloaded by ReadDirectoryFiles())
|
|||
FD_RELOAD_DIRPATH(state); |
|||
|
|||
strcpy(state->dirPathTextCopy, state->dirPathText); |
|||
} |
|||
else strcpy(state->dirPathText, state->dirPathTextCopy); |
|||
} |
|||
|
|||
state->dirPathEditMode = !state->dirPathEditMode; |
|||
} |
|||
|
|||
int prevTextAlignment = GuiGetStyle(LISTVIEW, TEXT_ALIGNMENT); |
|||
int prevElementsHeight = GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT); |
|||
GuiSetStyle(LISTVIEW, TEXT_ALIGNMENT, GUI_TEXT_ALIGN_LEFT); |
|||
GuiSetStyle(LISTVIEW, LIST_ITEMS_HEIGHT, 24); |
|||
|
|||
// TODO: ListViewElements should be aligned left
|
|||
# if defined(USE_CUSTOM_LISTVIEW_FILEINFO) |
|||
FileInfo fileInfo; |
|||
state->filesListActive = GuiListViewFiles((Rectangle){ state->position.x + 10, state->position.y + 70, winWidth - 20, winHeight - 135 }, fileInfo, state->dirFilesCount, &state->itemFocused, &state->filesListScrollIndex, state->filesListActive); |
|||
# else |
|||
state->filesListActive = GuiListViewEx((Rectangle){ state->position.x + 10, state->position.y + 70, winWidth - 20, winHeight - 135 }, (const char**)dirFilesIcon, state->dirFilesCount, &state->itemFocused, &state->filesListScrollIndex, state->filesListActive); |
|||
# endif |
|||
GuiSetStyle(LISTVIEW, TEXT_ALIGNMENT, prevTextAlignment); |
|||
GuiSetStyle(LISTVIEW, LIST_ITEMS_HEIGHT, prevElementsHeight); |
|||
|
|||
if ((state->filesListActive >= 0) && (state->filesListActive != state->prevFilesListActive)) |
|||
//&& (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) || IsKeyPressed(KEY_ENTER) || IsKeyPressed(KEY_DPAD_A)))
|
|||
{ |
|||
strcpy(state->fileNameText, state->dirFiles[state->filesListActive]); |
|||
|
|||
if (DirectoryExists(TextFormat("%s/%s", state->dirPathText, state->fileNameText))) |
|||
{ |
|||
if (TextIsEqual(state->fileNameText, "..")) strcpy(state->dirPathText, GetPrevDirectoryPath(state->dirPathText)); |
|||
else strcpy(state->dirPathText, TextFormat("%s/%s", strcmp(state->dirPathText, "/")==0 ? "" : state->dirPathText, state->fileNameText)); |
|||
|
|||
strcpy(state->dirPathTextCopy, state->dirPathText); |
|||
|
|||
// RL_FREE previous dirFiles (reloaded by ReadDirectoryFiles())
|
|||
FD_RELOAD_DIRPATH(state); |
|||
|
|||
strcpy(state->dirPathTextCopy, state->dirPathText); |
|||
|
|||
state->filesListActive = -1; |
|||
strcpy(state->fileNameText, "\0"); |
|||
strcpy(state->fileNameTextCopy, state->fileNameText); |
|||
} |
|||
|
|||
state->prevFilesListActive = state->filesListActive; |
|||
} |
|||
|
|||
GuiLabel((Rectangle){ state->position.x + 10, state->position.y + winHeight - 60, 68, 25 }, "File name:"); |
|||
|
|||
if (GuiTextBox((Rectangle){ state->position.x + 75, state->position.y + winHeight - 60, winWidth - 200, 25 }, state->fileNameText, 128, state->fileNameEditMode)) |
|||
{ |
|||
if (*state->fileNameText) |
|||
{ |
|||
// Verify if a valid filename has been introduced
|
|||
if (FileExists(TextFormat("%s/%s", state->dirPathText, state->fileNameText))) |
|||
{ |
|||
// Select filename from list view
|
|||
for (int i = 0; i < state->dirFilesCount; i++) |
|||
{ |
|||
if (TextIsEqual(state->fileNameText, state->dirFiles[i])) |
|||
{ |
|||
state->filesListActive = i; |
|||
strcpy(state->fileNameTextCopy, state->fileNameText); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
strcpy(state->fileNameText, state->fileNameTextCopy); |
|||
} |
|||
} |
|||
|
|||
state->fileNameEditMode = !state->fileNameEditMode; |
|||
} |
|||
|
|||
state->fileTypeActive = GuiComboBox((Rectangle){ state->position.x + 75, state->position.y + winHeight - 30, winWidth - 200, 25 }, "All files", state->fileTypeActive); |
|||
GuiLabel((Rectangle){ state->position.x + 10, state->position.y + winHeight - 30, 68, 25 }, "File filter:"); |
|||
|
|||
state->SelectFilePressed = GuiButton((Rectangle){ state->position.x + winWidth - 120, state->position.y + winHeight - 60, 110, |
|||
#ifdef PLATFORM_DESKTOP |
|||
25 |
|||
#else |
|||
25+30 |
|||
#endif |
|||
}, "Select");// || IsKeyPressed(KEY_ENTER) || IsKeyPressed(KEY_DPAD_A);
|
|||
|
|||
if (state->SelectFilePressed) state->fileDialogActive = false; |
|||
|
|||
#ifdef PLATFORM_DESKTOP |
|||
if (GuiButton((Rectangle){ state->position.x + winWidth - 120, state->position.y + winHeight - 30, 110, 25 }, "Quit")) state->fileDialogActive = false; |
|||
#endif |
|||
|
|||
// File dialog has been closed!
|
|||
if (!state->fileDialogActive) |
|||
{ |
|||
// RL_FREE dirFiles memory
|
|||
for (int i = 0; i < state->dirFilesCount; i++) |
|||
{ |
|||
RL_FREE(state->dirFiles[i]); |
|||
RL_FREE(dirFilesIcon[i]); |
|||
} |
|||
|
|||
RL_FREE(state->dirFiles); |
|||
RL_FREE(dirFilesIcon); |
|||
|
|||
dirFilesIcon = NULL; |
|||
state->dirFiles = NULL; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Read all filenames from directory (supported file types)
|
|||
static inline int _file_comp(const char *d1, const char *d2, const char *dir) |
|||
{ |
|||
const bool b1 = DirectoryExists(TextFormat("%s/%s", dir, d1)); |
|||
const bool b2 = DirectoryExists(TextFormat("%s/%s", dir, d2)); |
|||
|
|||
if (b1 && !b2) return -1; |
|||
if (!b1 && b2) return 1; |
|||
|
|||
if (!FileExists(TextFormat("%s/%s", dir, d1))) return 1; |
|||
if (!FileExists(TextFormat("%s/%s", dir, d2))) return -1; |
|||
|
|||
return strcmp(d1, d2); |
|||
} |
|||
static char **ReadDirectoryFiles(const char *dir, int *filesCount, char *filterExt) |
|||
{ |
|||
int validFilesCount = 0; |
|||
char **validFiles = (char **)RL_MALLOC(MAX_DIRECTORY_FILES*sizeof(char *)); // Max files to read
|
|||
for (int i = 0; i < MAX_DIRECTORY_FILES; i++) validFiles[i] = (char *)RL_MALLOC(MAX_DIR_PATH_LENGTH); // Max file name length
|
|||
|
|||
int filterExtCount = 0; |
|||
const char **extensions = GuiTextSplit(filterExt, &filterExtCount, NULL); |
|||
bool filterExtensions = true; |
|||
|
|||
int dirFilesCount = 0; |
|||
char **files = GetDirectoryFiles(dir, &dirFilesCount); |
|||
|
|||
// Sort files and directories: dir by name + files by name
|
|||
// https://en.wikibooks.org/wiki/Algorithm_Implementation/Sorting/Quicksort#C
|
|||
if (dirFilesCount > 1) |
|||
{ |
|||
const int MAX = 64; |
|||
unsigned int left = 0, stack[64], pos = 0, seed = rand(), len = dirFilesCount; |
|||
|
|||
for ( ; ; ) |
|||
{ |
|||
for (; left+1 < len; len++) /* sort left to len-1 */ |
|||
{ |
|||
if (pos == MAX) len = stack[pos = 0]; /* stack overflow, reset */ |
|||
char *pivot = files[left+seed%(len-left)]; /* pick random pivot */ |
|||
seed = seed*69069+1; /* next pseudorandom number */ |
|||
stack[pos++] = len; /* sort right part later */ |
|||
|
|||
for (unsigned int right = left-1; ; ) /* inner loop: partitioning */ |
|||
{ |
|||
while (_file_comp(files[++right], pivot, dir) < 0);/* look for greater element */ |
|||
while (_file_comp(pivot, files[--len], dir) < 0); /* look for smaller element */ |
|||
if (right >= len) break; /* partition point found? */ |
|||
char *temp = files[right]; |
|||
files[right] = files[len]; /* the only swap */ |
|||
files[len] = temp; |
|||
} /* partitioned, continue left part */ |
|||
} |
|||
|
|||
if (pos == 0) break; /* stack empty? */ |
|||
left = len; /* left to right is sorted */ |
|||
len = stack[--pos]; /* get next range to sort */ |
|||
} |
|||
} |
|||
|
|||
if (TextIsEqual(extensions[0], "all")) filterExtensions = false; |
|||
|
|||
for (int i = 0; (i < dirFilesCount) && (validFilesCount < MAX_DIRECTORY_FILES); i++) |
|||
{ |
|||
if (TextIsEqual(files[i], ".")) continue; |
|||
|
|||
if (!filterExtensions) |
|||
{ |
|||
strncpy(validFiles[validFilesCount], files[i], MAX_DIR_PATH_LENGTH); |
|||
|
|||
// Only filter files by extensions, directories should be available
|
|||
if (DirectoryExists(TextFormat("%s/%s", dir, files[i]))) strcpy(dirFilesIcon[validFilesCount], TextFormat("%s", files[i])); |
|||
else |
|||
{ |
|||
// TODO: Assign custom filetype icons depending on file extension (image, audio, text, video, models...)
|
|||
|
|||
if (IsFileExtension(files[i], ".png")) strcpy(dirFilesIcon[validFilesCount], TextFormat("%s", files[i])); |
|||
else strcpy(dirFilesIcon[validFilesCount], TextFormat("%s", files[i])); |
|||
} |
|||
|
|||
validFilesCount++; |
|||
} |
|||
else |
|||
{ |
|||
for (int j = 0; j < filterExtCount; j++) |
|||
{ |
|||
// Check file type extensions supported
|
|||
// NOTE: We just store valid files list
|
|||
if (IsFileExtension(files[i], extensions[j])) |
|||
{ |
|||
// TODO: Assign custom filetype icons depending on file extension (image, audio, text, video, models...)
|
|||
|
|||
if (IsFileExtension(files[i], ".png")) strcpy(dirFilesIcon[validFilesCount], TextFormat("%s", files[i])); |
|||
else strcpy(dirFilesIcon[validFilesCount], TextFormat("%s", files[i])); |
|||
|
|||
validFilesCount++; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
ClearDirectoryFiles(); |
|||
|
|||
*filesCount = validFilesCount; |
|||
return validFiles; |
|||
} |
|||
|
|||
#if defined(USE_CUSTOM_LISTVIEW_FILEINFO) |
|||
// List View control for files info with extended parameters
|
|||
static int GuiListViewFiles(Rectangle bounds, FileInfo *files, int count, int *focus, int *scrollIndex, int active) |
|||
{ |
|||
GuiControlState state = guiState; |
|||
int itemFocused = (focus == NULL)? -1 : *focus; |
|||
int itemSelected = active; |
|||
|
|||
// Check if we need a scroll bar
|
|||
bool useScrollBar = false; |
|||
if ((GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) + GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING))*count > bounds.height) useScrollBar = true; |
|||
|
|||
// Define base item rectangle [0]
|
|||
Rectangle itemBounds = { 0 }; |
|||
itemBounds.x = bounds.x + GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING); |
|||
itemBounds.y = bounds.y + GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING) + GuiGetStyle(DEFAULT, BORDER_WIDTH); |
|||
itemBounds.width = bounds.width - 2*GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING) - GuiGetStyle(DEFAULT, BORDER_WIDTH); |
|||
itemBounds.height = GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT); |
|||
if (useScrollBar) itemBounds.width -= GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH); |
|||
|
|||
// Get items on the list
|
|||
int visibleItems = bounds.height/(GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) + GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING)); |
|||
if (visibleItems > count) visibleItems = count; |
|||
|
|||
int startIndex = (scrollIndex == NULL)? 0 : *scrollIndex; |
|||
if ((startIndex < 0) || (startIndex > (count - visibleItems))) startIndex = 0; |
|||
int endIndex = startIndex + visibleItems; |
|||
|
|||
// Update control
|
|||
//--------------------------------------------------------------------
|
|||
if ((state != GUI_STATE_DISABLED) && !guiLocked) |
|||
{ |
|||
Vector2 mousePoint = GetMousePosition(); |
|||
|
|||
// Check mouse inside list view
|
|||
if (CheckCollisionPointRec(mousePoint, bounds)) |
|||
{ |
|||
state = GUI_STATE_FOCUSED; |
|||
|
|||
// Check focused and selected item
|
|||
for (int i = 0; i < visibleItems; i++) |
|||
{ |
|||
if (CheckCollisionPointRec(mousePoint, itemBounds)) |
|||
{ |
|||
itemFocused = startIndex + i; |
|||
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) itemSelected = startIndex + i; |
|||
break; |
|||
} |
|||
|
|||
// Update item rectangle y position for next item
|
|||
itemBounds.y += (GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) + GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING)); |
|||
} |
|||
|
|||
if (useScrollBar) |
|||
{ |
|||
int wheelMove = GetMouseWheelMove(); |
|||
startIndex -= wheelMove; |
|||
|
|||
if (startIndex < 0) startIndex = 0; |
|||
else if (startIndex > (count - visibleItems)) startIndex = count - visibleItems; |
|||
|
|||
endIndex = startIndex + visibleItems; |
|||
if (endIndex > count) endIndex = count; |
|||
} |
|||
} |
|||
else itemFocused = -1; |
|||
|
|||
// Reset item rectangle y to [0]
|
|||
itemBounds.y = bounds.y + GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING) + GuiGetStyle(DEFAULT, BORDER_WIDTH); |
|||
} |
|||
//--------------------------------------------------------------------
|
|||
|
|||
// Draw control
|
|||
//--------------------------------------------------------------------
|
|||
DrawRectangleRec(bounds, GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR))); // Draw background
|
|||
DrawRectangleLinesEx(bounds, GuiGetStyle(DEFAULT, BORDER_WIDTH), Fade(GetColor(GuiGetStyle(LISTVIEW, BORDER + state*3)), guiAlpha)); |
|||
|
|||
// TODO: Draw list view header with file sections: icon+name | size | type | modTime
|
|||
|
|||
// Draw visible items
|
|||
for (int i = 0; i < visibleItems; i++) |
|||
{ |
|||
if (state == GUI_STATE_DISABLED) |
|||
{ |
|||
if ((startIndex + i) == itemSelected) |
|||
{ |
|||
DrawRectangleRec(itemBounds, Fade(GetColor(GuiGetStyle(LISTVIEW, BASE_COLOR_DISABLED)), guiAlpha)); |
|||
DrawRectangleLinesEx(itemBounds, GuiGetStyle(LISTVIEW, BORDER_WIDTH), Fade(GetColor(GuiGetStyle(LISTVIEW, BORDER_COLOR_DISABLED)), guiAlpha)); |
|||
} |
|||
|
|||
// TODO: Draw full file info line: icon+name | size | type | modTime
|
|||
|
|||
GuiDrawText(files[startIndex + i].name, GetTextBounds(DEFAULT, itemBounds), GuiGetStyle(LISTVIEW, TEXT_ALIGNMENT), Fade(GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_DISABLED)), guiAlpha)); |
|||
} |
|||
else |
|||
{ |
|||
if ((startIndex + i) == itemSelected) |
|||
{ |
|||
// Draw item selected
|
|||
DrawRectangleRec(itemBounds, Fade(GetColor(GuiGetStyle(LISTVIEW, BASE_COLOR_PRESSED)), guiAlpha)); |
|||
DrawRectangleLinesEx(itemBounds, GuiGetStyle(LISTVIEW, BORDER_WIDTH), Fade(GetColor(GuiGetStyle(LISTVIEW, BORDER_COLOR_PRESSED)), guiAlpha)); |
|||
|
|||
GuiDrawText(files[startIndex + i].name, GetTextBounds(DEFAULT, itemBounds), GuiGetStyle(LISTVIEW, TEXT_ALIGNMENT), Fade(GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_PRESSED)), guiAlpha)); |
|||
} |
|||
else if ((startIndex + i) == itemFocused) |
|||
{ |
|||
// Draw item focused
|
|||
DrawRectangleRec(itemBounds, Fade(GetColor(GuiGetStyle(LISTVIEW, BASE_COLOR_FOCUSED)), guiAlpha)); |
|||
DrawRectangleLinesEx(itemBounds, GuiGetStyle(LISTVIEW, BORDER_WIDTH), Fade(GetColor(GuiGetStyle(LISTVIEW, BORDER_COLOR_FOCUSED)), guiAlpha)); |
|||
|
|||
GuiDrawText(files[startIndex + i].name, GetTextBounds(DEFAULT, itemBounds), GuiGetStyle(LISTVIEW, TEXT_ALIGNMENT), Fade(GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_FOCUSED)), guiAlpha)); |
|||
} |
|||
else |
|||
{ |
|||
// Draw item normal
|
|||
GuiDrawText(files[startIndex + i].name, GetTextBounds(DEFAULT, itemBounds), GuiGetStyle(LISTVIEW, TEXT_ALIGNMENT), Fade(GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_NORMAL)), guiAlpha)); |
|||
} |
|||
} |
|||
|
|||
// Update item rectangle y position for next item
|
|||
itemBounds.y += (GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) + GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING)); |
|||
} |
|||
|
|||
if (useScrollBar) |
|||
{ |
|||
Rectangle scrollBarBounds = { |
|||
bounds.x + bounds.width - GuiGetStyle(LISTVIEW, BORDER_WIDTH) - GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH), |
|||
bounds.y + GuiGetStyle(LISTVIEW, BORDER_WIDTH), (float)GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH), |
|||
bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) |
|||
}; |
|||
|
|||
// Calculate percentage of visible items and apply same percentage to scrollbar
|
|||
float percentVisible = (float)(endIndex - startIndex)/count; |
|||
float sliderSize = bounds.height*percentVisible; |
|||
|
|||
int prevSliderSize = GuiGetStyle(SCROLLBAR, SLIDER_WIDTH); // Save default slider size
|
|||
int prevScrollSpeed = GuiGetStyle(SCROLLBAR, SCROLL_SPEED); // Save default scroll speed
|
|||
GuiSetStyle(SCROLLBAR, SLIDER_WIDTH, sliderSize); // Change slider size
|
|||
GuiSetStyle(SCROLLBAR, SCROLL_SPEED, count - visibleItems); // Change scroll speed
|
|||
|
|||
startIndex = GuiScrollBar(scrollBarBounds, startIndex, 0, count - visibleItems); |
|||
|
|||
GuiSetStyle(SCROLLBAR, SCROLL_SPEED, prevScrollSpeed); // Reset scroll speed to default
|
|||
GuiSetStyle(SCROLLBAR, SLIDER_WIDTH, prevSliderSize); // Reset slider size to default
|
|||
} |
|||
//--------------------------------------------------------------------
|
|||
|
|||
if (focus != NULL) *focus = itemFocused; |
|||
if (scrollIndex != NULL) *scrollIndex = startIndex; |
|||
|
|||
return itemSelected; |
|||
} |
|||
#endif // USE_CUSTOM_LISTVIEW_FILEINFO
|
|||
|
|||
#endif // GUI_FILE_DIALOG_IMPLEMENTATION
|
@ -0,0 +1,789 @@ |
|||
//Author: Tony Wilk
|
|||
//Source: https://www.codeproject.com/Articles/885389/jRead-An-in-place-JSON-Element-Reader
|
|||
//LICENSE: The Code Project Open License (CPOL) 1.02
|
|||
//see provided file ICENSE_jReadJWrite.html or https://www.codeproject.com/info/cpol10.aspx
|
|||
|
|||
// jRead.cpp
|
|||
// Version 1v6
|
|||
//
|
|||
// jRead - an in-place JSON element reader
|
|||
// =======================================
|
|||
//
|
|||
// Instead of parsing JSON into some structure, this maintains the input JSON as unaltered text
|
|||
// and allows queries to be made on it directly.
|
|||
//
|
|||
// e.g. with the simple JSON:
|
|||
// {
|
|||
// "astring":"This is a string",
|
|||
// "anumber":42,
|
|||
// "myarray":[ "one", 2, {"description":"element 3"}, null ],
|
|||
// "yesno":true,
|
|||
// "HowMany":"1234",
|
|||
// "foo":null
|
|||
// }
|
|||
//
|
|||
// calling:
|
|||
// jRead( json, "{'myarray'[0", &jElem );
|
|||
//
|
|||
// would return:
|
|||
// jElem.dataType= JREAD_STRING;
|
|||
// jElem.elements= 1
|
|||
// jElem.bytelen= 3
|
|||
// jElem.pValue -> "one"
|
|||
//
|
|||
// or you could call the helper functions:
|
|||
// jRead_string( json, "{'astring'", destString, MAXLEN );
|
|||
// jRead_int( json, "{'anumber'", &myint );
|
|||
// jRead_string( json, "{'myarray'[3", destString, MAXLEN );
|
|||
// etc.
|
|||
//
|
|||
// Note that the helper functions do type coersion and always return a value
|
|||
// (on error an empty string is returned or value of zero etc.)
|
|||
//
|
|||
// The query string simply defines the route to the required data item
|
|||
// as an arbitary list of object or array specifiers:
|
|||
// object element= "{'keyname'"
|
|||
// array element= "[INDEX"
|
|||
//
|
|||
// The jRead() function fills a jReadElement structure to describe the located element
|
|||
// this can be used to locate any element, not just terminal values
|
|||
// e.g.
|
|||
// jRead( json, "{'myarray'", &jElem );
|
|||
//
|
|||
// in this case jElem would contain:
|
|||
// jElem.dataType= JSON_ARRAY
|
|||
// jElem.elements= 4
|
|||
// jElem.bytelen= 46
|
|||
// jElem.pValue -> [ "one", 2, {"descripton":"element 3"}, null ] ...
|
|||
//
|
|||
// allowing jRead to be called again on the array:
|
|||
// e.g.
|
|||
// jRead( jElem.pValue, "[3", &jElem ); // get 4th element - the null value
|
|||
//
|
|||
// .oO! see main.c runExamples() for a whole bunch of examples !Oo.
|
|||
// -------------------------------------------------------
|
|||
//
|
|||
// Note that jRead never modifies the source JSON and does not allocate any memory.
|
|||
// i.e. elements are returned as pointer and length into the source text.
|
|||
//
|
|||
// Functions
|
|||
// =========
|
|||
// Main JSON reader:
|
|||
// int jRead( char * JsonSource, char *query, jReadElement &pResult );
|
|||
//
|
|||
// Extended function using query parameters for indexing:
|
|||
// int jRead( char * JsonSource, char *query, jReadElement &pResult, int *queryParams );
|
|||
//
|
|||
// Function to step thru JSON arrays instead of indexing:
|
|||
// char *jReadArrayStep( char *pJsonArray, struct jReadElement *pResult );
|
|||
//
|
|||
// Optional Helper functions:
|
|||
// long jRead_long( char *pJson, char *pQuery );
|
|||
// int jRead_int( char *pJson, char *pQuery );
|
|||
// double jRead_double( char *pJson, char *pQuery );
|
|||
// int jRead_string( char *pJson, char *pQuery, char *pDest, int destlen );
|
|||
//
|
|||
// Optional String output Functions
|
|||
// char * jReadTypeToString( int dataType ); // string describes dataType
|
|||
// char * jReadErrorToString( int error ); // string descibes error code
|
|||
//
|
|||
// *NEW* in 1v2
|
|||
// - "{NUMBER" returns the "key" value at that index within an object
|
|||
// - jReadParam() adds queryParams which can be used as indexes into arrays (or into
|
|||
// objects to return key values) by specifying '*' in the query string
|
|||
// e.g. jReadParam( pJson, "[*", &result, &index )
|
|||
// *NEW in 1v4
|
|||
// - fixed a couple of error return values
|
|||
// - added #define JREAD_DOUBLE_QUOTE_IN_QUERY
|
|||
// *NEW* in 1v5 (11mar2015)
|
|||
// - fixed null ptr if '[*' used when null param passed
|
|||
// *NEW* in 1v6 (24sep2016)
|
|||
// - fixed handling of empty arrays and objects
|
|||
//
|
|||
// TonyWilk, 24sep2016
|
|||
// mail at tonywilk . co .uk
|
|||
//
|
|||
// License: "Free as in You Owe Me a Beer"
|
|||
// - actually, since some people really worry about licenses, you are free to apply
|
|||
// whatever licence you want.
|
|||
//
|
|||
// Note: jRead_atol() and jRead_atof() are modified from original routines
|
|||
// fast_atol() and fast_atof() 09-May-2009 Tom Van Baak (tvb) www.LeapSecond.com
|
|||
//
|
|||
// You may want to replace the use of jRead_atol() and jRead_atof() in helper functions
|
|||
// of your own. Especially note that my atof does not handle exponents.
|
|||
//
|
|||
//
|
|||
#include <stdio.h> |
|||
|
|||
#include "jRead.h" |
|||
|
|||
// By default we use single quote in query strings so it's a lot easier
|
|||
// to type in code i.e. "{'key'" instead of "{\"key\""
|
|||
//
|
|||
#ifdef JREAD_DOUBLE_QUOTE_IN_QUERY |
|||
#define QUERY_QUOTE '\"' |
|||
#else |
|||
#define QUERY_QUOTE '\'' |
|||
#endif |
|||
|
|||
//------------------------------------------------------
|
|||
// Internal Functions
|
|||
|
|||
char * jReadSkipWhitespace( char *sp ); |
|||
char * jReadFindTok( char *sp, int *tokType ); |
|||
char * jReadGetString( char *pJson, struct jReadElement *pElem, char quote ); |
|||
int jReadTextLen( char *pJson ); |
|||
int jReadStrcmp( struct jReadElement *j1, struct jReadElement *j2 ); |
|||
char * jReadCountObject( char *pJson, struct jReadElement *pResult, int keyIndex ); |
|||
char * jReadCountArray( char *pJson, struct jReadElement *pResult ); |
|||
char * jRead_atoi( char *p, unsigned int *result ); |
|||
char * jRead_atol( char *p, long *result ); |
|||
char * jRead_atof( char *p, double *result); |
|||
|
|||
//=======================================================
|
|||
|
|||
char *jReadSkipWhitespace( char *sp ) |
|||
{ |
|||
while( (*sp != '\0') && (*sp <= ' ') ) |
|||
sp++; |
|||
return sp; |
|||
}; |
|||
|
|||
|
|||
// Find start of a token
|
|||
// - returns pointer to start of next token or element
|
|||
// returns type via tokType
|
|||
//
|
|||
char *jReadFindTok( char *sp, int *tokType ) |
|||
{ |
|||
char c; |
|||
sp= jReadSkipWhitespace(sp); |
|||
c= *sp; |
|||
if( c == '\0' ) *tokType= JREAD_EOL; |
|||
else if((c == '"') || (c == QUERY_QUOTE))*tokType= JREAD_STRING; |
|||
else if((c >= '0') && (c <= '9')) *tokType= JREAD_NUMBER; |
|||
else if( c == '-') *tokType= JREAD_NUMBER; |
|||
else if( c == '{') *tokType= JREAD_OBJECT; |
|||
else if( c == '[') *tokType= JREAD_ARRAY; |
|||
else if( c == '}') *tokType= JREAD_EOBJECT; |
|||
else if( c == ']') *tokType= JREAD_EARRAY; |
|||
else if((c == 't') || (c == 'f')) *tokType= JREAD_BOOL; |
|||
else if( c == 'n') *tokType= JREAD_NULL; |
|||
else if( c == ':') *tokType= JREAD_COLON; |
|||
else if( c == ',') *tokType= JREAD_COMMA; |
|||
else if( c == '*') *tokType= JREAD_QPARAM; |
|||
else *tokType= JREAD_ERROR; |
|||
return sp; |
|||
}; |
|||
|
|||
// jReadGetString
|
|||
// - assumes next element is "string" which may include "\" sequences
|
|||
// - returns pointer to -------------^
|
|||
// - pElem contains result ( JREAD_STRING, length, pointer to string)
|
|||
// - pass quote = '"' for Json, quote = '\'' for query scanning
|
|||
//
|
|||
// returns: pointer into pJson after the string (char after the " terminator)
|
|||
// pElem contains pointer and length of string (or dataType=JREAD_ERROR)
|
|||
//
|
|||
char * jReadGetString( char *pJson, struct jReadElement *pElem, char quote ) |
|||
{ |
|||
short skipch; |
|||
pElem->dataType= JREAD_ERROR; |
|||
pElem->elements= 1; |
|||
pElem->bytelen= 0; |
|||
pJson= jReadSkipWhitespace( pJson ); |
|||
if( *pJson == quote ) |
|||
{ |
|||
pJson++; |
|||
pElem->pValue= pJson; // -> start of actual string
|
|||
pElem->bytelen=0; |
|||
skipch= 0; |
|||
while( *pJson != '\0' ) |
|||
{ |
|||
if( skipch ) |
|||
skipch= 0; |
|||
else if( *pJson == '\\' ) // "\" sequence
|
|||
skipch= 1; |
|||
else if( *pJson == quote ) |
|||
{ |
|||
pElem->dataType= JREAD_STRING; |
|||
pJson++; |
|||
break; |
|||
} |
|||
pElem->bytelen++; |
|||
pJson++; |
|||
}; |
|||
}; |
|||
return pJson; |
|||
}; |
|||
|
|||
// jReadTextLen
|
|||
// - used to identify length of element text
|
|||
// - returns no. of chars from pJson upto a terminator
|
|||
// - terminators: ' ' , } ]
|
|||
//
|
|||
int jReadTextLen( char *pJson ) |
|||
{ |
|||
int len= 0; |
|||
while( (*pJson > ' ' ) && // any ctrl char incl '\0'
|
|||
(*pJson != ',' ) && |
|||
(*pJson != '}' ) && |
|||
(*pJson != ']' ) ) |
|||
{ |
|||
len++; |
|||
pJson++; |
|||
} |
|||
return len; |
|||
} |
|||
|
|||
// compare two json elements
|
|||
// returns: 0 if they are identical strings, else 1
|
|||
//
|
|||
int jReadStrcmp( struct jReadElement *j1, struct jReadElement *j2 ) |
|||
{ |
|||
int i; |
|||
if( (j1->dataType != JREAD_STRING) || |
|||
(j2->dataType != JREAD_STRING) || |
|||
(j1->bytelen != j2->bytelen ) ) |
|||
return 1; |
|||
|
|||
for( i=0; i< j1->bytelen; i++ ) |
|||
if( ((char *)(j1->pValue))[i] != ((char *)(j2->pValue))[i] ) |
|||
return 1; |
|||
return 0; |
|||
} |
|||
|
|||
// read unsigned int from string
|
|||
char * jRead_atoi( char *p, unsigned int *result ) |
|||
{ |
|||
unsigned int x = 0; |
|||
while (*p >= '0' && *p <= '9') { |
|||
x = (x*10) + (*p - '0'); |
|||
++p; |
|||
} |
|||
*result= x; |
|||
return p; |
|||
} |
|||
|
|||
// read long int from string
|
|||
//
|
|||
char * jRead_atol( char *p, long *result ) |
|||
{ |
|||
long x = 0; |
|||
int neg = 0; |
|||
if (*p == '-') { |
|||
neg = 1; |
|||
++p; |
|||
} |
|||
while (*p >= '0' && *p <= '9') { |
|||
x = (x*10) + (*p - '0'); |
|||
++p; |
|||
} |
|||
if (neg) { |
|||
x = -x; |
|||
} |
|||
*result= x; |
|||
return p; |
|||
} |
|||
|
|||
|
|||
#define valid_digit(c) ((c) >= '0' && (c) <= '9') |
|||
|
|||
// read double from string
|
|||
// *CAUTION* does not handle exponents
|
|||
//
|
|||
//
|
|||
char * jRead_atof( char *p, double *result) |
|||
{ |
|||
double sign, value; |
|||
|
|||
// Get sign, if any.
|
|||
sign = 1.0; |
|||
if (*p == '-') { |
|||
sign = -1.0; |
|||
p += 1; |
|||
|
|||
} else if (*p == '+') { |
|||
p += 1; |
|||
} |
|||
|
|||
// Get digits before decimal point or exponent, if any.
|
|||
for (value = 0.0; valid_digit(*p); p += 1) { |
|||
value = value * 10.0 + (*p - '0'); |
|||
} |
|||
|
|||
// Get digits after decimal point, if any.
|
|||
if (*p == '.') { |
|||
double pow10 = 10.0; |
|||
p += 1; |
|||
while (valid_digit(*p)) { |
|||
value += (*p - '0') / pow10; |
|||
pow10 *= 10.0; |
|||
p += 1; |
|||
} |
|||
} |
|||
*result= sign * value; |
|||
return p; |
|||
} |
|||
|
|||
// read element into destination buffer and add '\0' terminator
|
|||
// - always copies element irrespective of dataType (unless it's an error)
|
|||
// - destBuffer is always '\0'-terminated (even on zero lenght returns)
|
|||
// - returns pointer to destBuffer
|
|||
//
|
|||
char *jRead_strcpy( char *destBuffer, int destLength, struct jReadElement *pElement ) |
|||
{ |
|||
int i; |
|||
int len= pElement->bytelen; |
|||
char *pdest= destBuffer; |
|||
char *psrc= (char *)pElement->pValue; |
|||
if( pElement->error == 0 ) |
|||
{ |
|||
if( len >= destLength ) |
|||
len= destLength; |
|||
for( i=0; i<destLength; i++ ) |
|||
*pdest++= *psrc++; |
|||
} |
|||
*pdest= '\0'; |
|||
return destBuffer; |
|||
} |
|||
|
|||
// jReadCountObject
|
|||
// - used when query ends at an object, we want to return the object length
|
|||
// - on entry pJson -> "{... "
|
|||
// - used to skip unwanted values which are objects
|
|||
// - keyIndex normally passed as -1 unless we're looking for the nth "key" value
|
|||
// in which case keyIndex is the index of the key we want
|
|||
//
|
|||
char * jReadCountObject( char *pJson, struct jReadElement *pResult, int keyIndex ) |
|||
{ |
|||
struct jReadElement jElement; |
|||
int jTok; |
|||
char *sp; |
|||
pResult->dataType= JREAD_OBJECT; |
|||
pResult->error= 0; |
|||
pResult->elements= 0; |
|||
pResult->pValue= pJson; |
|||
sp= jReadFindTok( pJson+1, &jTok ); // check for empty object
|
|||
if( jTok == JREAD_EOBJECT ) |
|||
{ |
|||
pJson= sp+1; |
|||
}else |
|||
{ |
|||
while( 1 ) |
|||
{ |
|||
pJson= jReadGetString( ++pJson, &jElement, '\"' ); |
|||
if( jElement.dataType != JREAD_STRING ) |
|||
{ |
|||
pResult->error= 3; // Expected "key"
|
|||
break; |
|||
} |
|||
if( pResult->elements == keyIndex ) // if passed keyIndex
|
|||
{ |
|||
*pResult= jElement; // we return "key" at this index
|
|||
pResult->dataType= JREAD_KEY; |
|||
return pJson; |
|||
} |
|||
pJson= jReadFindTok( pJson, &jTok ); |
|||
if( jTok != JREAD_COLON ) |
|||
{ |
|||
pResult->error= 4; // Expected ":"
|
|||
break; |
|||
} |
|||
pJson= jRead( ++pJson, "", &jElement ); |
|||
if( pResult->error ) |
|||
break; |
|||
pJson= jReadFindTok( pJson, &jTok ); |
|||
pResult->elements++; |
|||
if( jTok == JREAD_EOBJECT ) |
|||
{ |
|||
pJson++; |
|||
break; |
|||
} |
|||
if( jTok != JREAD_COMMA ) |
|||
{ |
|||
pResult->error= 6; // Expected "," in object
|
|||
break; |
|||
} |
|||
} |
|||
} |
|||
if( keyIndex >= 0 ) |
|||
{ |
|||
// we wanted a "key" value - that we didn't find
|
|||
pResult->dataType= JREAD_ERROR; |
|||
pResult->error= 11; // Object key not found (bad index)
|
|||
}else{ |
|||
pResult->bytelen= pJson - (char *)pResult->pValue; |
|||
} |
|||
return pJson; |
|||
} |
|||
|
|||
|
|||
|
|||
// jReadCountArray
|
|||
// - used when query ends at an array, we want to return the array length
|
|||
// - on entry pJson -> "[... "
|
|||
// - used to skip unwanted values which are arrays
|
|||
//
|
|||
char * jReadCountArray( char *pJson, struct jReadElement *pResult ) |
|||
{ |
|||
struct jReadElement jElement; |
|||
int jTok; |
|||
char *sp; |
|||
pResult->dataType= JREAD_ARRAY; |
|||
pResult->error= 0; |
|||
pResult->elements= 0; |
|||
pResult->pValue= pJson; |
|||
sp= jReadFindTok( pJson+1, &jTok ); // check for empty array
|
|||
if( jTok == JREAD_EARRAY ) |
|||
{ |
|||
pJson= sp+1; |
|||
}else |
|||
{ |
|||
while( 1 ) |
|||
{ |
|||
pJson= jRead( ++pJson, "", &jElement ); // array value
|
|||
if( pResult->error ) |
|||
break; |
|||
pJson= jReadFindTok( pJson, &jTok ); // , or ]
|
|||
pResult->elements++; |
|||
if( jTok == JREAD_EARRAY ) |
|||
{ |
|||
pJson++; |
|||
break; |
|||
} |
|||
if( jTok != JREAD_COMMA ) |
|||
{ |
|||
pResult->error= 9; // Expected "," in array
|
|||
break; |
|||
} |
|||
} |
|||
} |
|||
pResult->bytelen= pJson - (char *)pResult->pValue; |
|||
return pJson; |
|||
} |
|||
|
|||
// jReadArrayStep()
|
|||
// - reads one value from an array
|
|||
// - assumes pJsonArray points at the start of an array or array element
|
|||
//
|
|||
char *jReadArrayStep( char *pJsonArray, struct jReadElement *pResult ) |
|||
{ |
|||
int jTok; |
|||
|
|||
pJsonArray= jReadFindTok( pJsonArray, &jTok ); |
|||
switch( jTok ) |
|||
{ |
|||
case JREAD_ARRAY: // start of array
|
|||
case JREAD_COMMA: // element separator
|
|||
return jRead( ++pJsonArray, "", pResult ); |
|||
|
|||
case JREAD_EARRAY: // end of array
|
|||
pResult->error= 13; // End of array found
|
|||
break; |
|||
default: // some other error
|
|||
pResult->error= 9; // expected comma in array
|
|||
break; |
|||
} |
|||
pResult->dataType= JREAD_ERROR; |
|||
return pJsonArray; |
|||
} |
|||
|
|||
|
|||
// jRead
|
|||
// - reads a complete JSON <value>
|
|||
// - matches pQuery against pJson, results in pResult
|
|||
// returns: pointer into pJson
|
|||
//
|
|||
// Note: is recursive
|
|||
//
|
|||
char * jRead( char *pJson, char *pQuery, struct jReadElement *pResult ) |
|||
{ |
|||
return jReadParam( pJson, pQuery, pResult, NULL ); |
|||
} |
|||
|
|||
char * jReadParam( char *pJson, char *pQuery, struct jReadElement *pResult, int *queryParams ) |
|||
{ |
|||
int qTok, jTok, bytelen; |
|||
unsigned int index, count; |
|||
struct jReadElement qElement, jElement; |
|||
|
|||
pJson= jReadFindTok( pJson, &jTok ); |
|||
pQuery= jReadFindTok( pQuery, &qTok ); |
|||
|
|||
pResult->dataType= jTok; |
|||
pResult->bytelen= pResult->elements= pResult->error= 0; |
|||
pResult->pValue= pJson; |
|||
|
|||
if( (qTok != JREAD_EOL) && (qTok != jTok) ) |
|||
{ |
|||
pResult->error= 1; // JSON does not match Query
|
|||
return pJson; |
|||
} |
|||
|
|||
switch( jTok ) |
|||
{ |
|||
case JREAD_ERROR: // general error, eof etc.
|
|||
pResult->error= 2; // Error reading JSON value
|
|||
break; |
|||
|
|||
case JREAD_OBJECT: // "{"
|
|||
if( qTok == JREAD_EOL ) |
|||
return jReadCountObject( pJson, pResult, -1 ); // return length of object
|
|||
|
|||
pQuery= jReadFindTok( ++pQuery, &qTok ); // "('key'...", "{NUMBER", "{*" or EOL
|
|||
if( qTok != JREAD_STRING ) |
|||
{ |
|||
index= 0; |
|||
switch( qTok ) |
|||
{ |
|||
case JREAD_NUMBER: |
|||
pQuery= jRead_atoi( (char *)pQuery, &index ); // index value
|
|||
break; |
|||
case JREAD_QPARAM: |
|||
pQuery++; |
|||
index= (queryParams != NULL) ? *queryParams++ : 0; // substitute parameter
|
|||
break; |
|||
default: |
|||
pResult->error= 12; // Bad Object key
|
|||
return pJson; |
|||
} |
|||
return jReadCountObject( pJson, pResult, index ); |
|||
} |
|||
|
|||
pQuery= jReadGetString( pQuery, &qElement, QUERY_QUOTE ); // qElement = query 'key'
|
|||
//
|
|||
// read <key> : <value> , ... }
|
|||
// loop 'til key matched
|
|||
//
|
|||
while( 1 ) |
|||
{ |
|||
pJson= jReadGetString( ++pJson, &jElement, '\"' ); |
|||
if( jElement.dataType != JREAD_STRING ) |
|||
{ |
|||
pResult->error= 3; // Expected "key"
|
|||
break; |
|||
} |
|||
pJson= jReadFindTok( pJson, &jTok ); |
|||
if( jTok != JREAD_COLON ) |
|||
{ |
|||
pResult->error= 4; // Expected ":"
|
|||
break; |
|||
} |
|||
// compare object keys
|
|||
if( jReadStrcmp( &qElement, &jElement ) == 0 ) |
|||
{ |
|||
// found object key
|
|||
return jReadParam( ++pJson, pQuery, pResult, queryParams ); |
|||
} |
|||
// no key match... skip this value
|
|||
pJson= jRead( ++pJson, "", pResult ); |
|||
pJson= jReadFindTok( pJson, &jTok ); |
|||
if( jTok == JREAD_EOBJECT ) |
|||
{ |
|||
pResult->error= 5; // Object key not found
|
|||
break; |
|||
} |
|||
if( jTok != JREAD_COMMA ) |
|||
{ |
|||
pResult->error= 6; // Expected "," in object
|
|||
break; |
|||
} |
|||
} |
|||
break; |
|||
case JREAD_ARRAY: // "[NUMBER" or "[*"
|
|||
//
|
|||
// read index, skip values 'til index
|
|||
//
|
|||
if( qTok == JREAD_EOL ) |
|||
return jReadCountArray( pJson, pResult ); // return length of object
|
|||
|
|||
index= 0; |
|||
pQuery= jReadFindTok( ++pQuery, &qTok ); // "[NUMBER" or "[*"
|
|||
if( qTok == JREAD_NUMBER ) |
|||
{ |
|||
pQuery= jRead_atoi( pQuery, &index ); // get array index
|
|||
}else if( qTok == JREAD_QPARAM ) |
|||
{ |
|||
pQuery++; |
|||
index= (queryParams != NULL) ? *queryParams++ : 0; // substitute parameter
|
|||
} |
|||
|
|||
count=0; |
|||
while( 1 ) |
|||
{ |
|||
if( count == index ) |
|||
return jReadParam( ++pJson, pQuery, pResult, queryParams ); // return value at index
|
|||
// not this index... skip this value
|
|||
pJson= jRead( ++pJson, "", &jElement ); |
|||
if( pResult->error ) |
|||
break; |
|||
count++; |
|||
pJson= jReadFindTok( pJson, &jTok ); // , or ]
|
|||
if( jTok == JREAD_EARRAY ) |
|||
{ |
|||
pResult->error= 10; // Array element not found (bad index)
|
|||
break; |
|||
} |
|||
if( jTok != JREAD_COMMA ) |
|||
{ |
|||
pResult->error= 9; // Expected "," in array
|
|||
break; |
|||
} |
|||
} |
|||
break; |
|||
case JREAD_STRING: // "string"
|
|||
pJson= jReadGetString( pJson, pResult, '\"' ); |
|||
break; |
|||
case JREAD_NUMBER: // number (may be -ve) int or float
|
|||
case JREAD_BOOL: // true or false
|
|||
case JREAD_NULL: // null
|
|||
bytelen= jReadTextLen( pJson ); |
|||
pResult->dataType= jTok; |
|||
pResult->bytelen= bytelen; |
|||
pResult->pValue= pJson; |
|||
pResult->elements= 1; |
|||
pJson += bytelen; |
|||
break; |
|||
default: |
|||
pResult->error= 8; // unexpected character (in pResult->dataType)
|
|||
} |
|||
// We get here on a 'terminal value'
|
|||
// - make sure the query string is empty also
|
|||
pQuery= jReadFindTok( pQuery, &qTok ); |
|||
if( !pResult->error && (qTok != JREAD_EOL) ) |
|||
pResult->error= 7; // terminal value found before end of query
|
|||
if( pResult->error ) |
|||
{ |
|||
pResult->dataType= JREAD_ERROR; |
|||
pResult->elements= pResult->bytelen= 0; |
|||
pResult->pValue= pJson; // return pointer into JSON at error point
|
|||
} |
|||
return pJson; |
|||
} |
|||
|
|||
|
|||
//--------------------------------------------------------------------
|
|||
// Optional helper functions
|
|||
// - simple routines to extract values from JSON
|
|||
// - does coercion of types where possible
|
|||
// - always returns a value (e.g. 0 or "" on error)
|
|||
//
|
|||
// Note: by default, pass NULL for queryParams
|
|||
// unless you are using '*' in the query for indexing
|
|||
//
|
|||
|
|||
// jRead_long
|
|||
// - reads signed long value from JSON
|
|||
// - returns number from NUMBER or STRING elements (if possible)
|
|||
// returns 1 or 0 from BOOL elements
|
|||
// otherwise returns 0
|
|||
//
|
|||
long jRead_long( char *pJson, char *pQuery, int *queryParams ) |
|||
{ |
|||
struct jReadElement elem; |
|||
long result; |
|||
jReadParam( pJson, pQuery, &elem, queryParams ); |
|||
if( (elem.dataType == JREAD_ERROR) || (elem.dataType == JREAD_NULL)) |
|||
return 0; |
|||
if( elem.dataType == JREAD_BOOL ) |
|||
return *((char *)elem.pValue)=='t' ? 1 : 0; |
|||
|
|||
jRead_atol( (char *)elem.pValue, &result ); |
|||
return result; |
|||
} |
|||
|
|||
int jRead_int( char *pJson, char *pQuery, int *queryParams ) |
|||
{ |
|||
return (int)jRead_long( pJson, pQuery, queryParams ); |
|||
} |
|||
|
|||
// jRead_double
|
|||
// - returns double from JSON
|
|||
// - returns number from NUMBER or STRING elements
|
|||
// otherwise returns 0.0
|
|||
//
|
|||
double jRead_double( char *pJson, char *pQuery, int *queryParams ) |
|||
{ |
|||
struct jReadElement elem; |
|||
double result; |
|||
jReadParam( pJson, pQuery, &elem, queryParams ); |
|||
if( elem.dataType == JREAD_ERROR ) |
|||
return 0.0; |
|||
jRead_atof( (char *)elem.pValue, &result ); |
|||
return result; |
|||
} |
|||
|
|||
// jRead_string
|
|||
// Copy string to pDest and '\0'-terminate it (upto destlen total bytes)
|
|||
// returns: character length of string (excluding '\0' terminator)
|
|||
//
|
|||
// Note: any element can be returned as a string
|
|||
//
|
|||
int jRead_string( char *pJson, char *pQuery, char *pDest, int destlen, int *queryParams ) |
|||
{ |
|||
struct jReadElement elem; |
|||
int i; |
|||
|
|||
*pDest= '\0'; |
|||
jReadParam( pJson, pQuery, &elem, queryParams ); |
|||
if( elem.dataType == JREAD_ERROR ) |
|||
return 0; |
|||
|
|||
for( i=0; (i<elem.bytelen) && (i<destlen-1); i++ ) |
|||
*pDest++ = ((char *)elem.pValue)[i]; |
|||
*pDest= '\0'; |
|||
return elem.bytelen; |
|||
} |
|||
|
|||
//-------------------------------------------------
|
|||
// Optional String output Functions
|
|||
//
|
|||
char *jReadTypeStrings[]={ |
|||
"Error", // 0
|
|||
"Object", // 1
|
|||
"Array", // 2
|
|||
"String", // 3
|
|||
"Number", // 4
|
|||
"Bool", // 5
|
|||
"null", // 6
|
|||
"Object key", // 7
|
|||
"colon", // 8
|
|||
"eol", // 9
|
|||
"comma", // 10
|
|||
"}", // 11
|
|||
"]", // 12
|
|||
"* parameter" // 13
|
|||
}; |
|||
|
|||
char *jReadTypeToString( int dataType ) |
|||
{ |
|||
return jReadTypeStrings[ dataType ]; |
|||
}; |
|||
|
|||
char * jReadErrorStrings[]={ |
|||
"Ok", // 0
|
|||
"JSON does not match Query", // 1
|
|||
"Error reading JSON value", // 2
|
|||
"Expected \"key\"", // 3
|
|||
"Expected ':'", // 4
|
|||
"Object key not found", // 5
|
|||
"Expected ',' in object", // 6
|
|||
"Terminal value found before end of query", // 7
|
|||
"Unexpected character", // 8
|
|||
"Expected ',' in array", // 9
|
|||
"Array element not found (bad index)", // 10
|
|||
"Object key not found (bad index)", // 11
|
|||
"Bad object key", // 12
|
|||
"End of array found", // 13
|
|||
"End of object found" // 14
|
|||
}; |
|||
char * jReadErrorToString( int error ) |
|||
{ |
|||
if( (error >=0 ) && (error <= 14)) |
|||
return jReadErrorStrings[ error ]; |
|||
return "Unknown error"; |
|||
}; |
|||
|
|||
// end of jRead.c
|
@ -0,0 +1,134 @@ |
|||
//Author: Tony Wilk
|
|||
//Source: https://www.codeproject.com/Articles/885389/jRead-An-in-place-JSON-Element-Reader
|
|||
//LICENSE: The Code Project Open License (CPOL) 1.02
|
|||
//see provided file ICENSE_jReadJWrite.html or https://www.codeproject.com/info/cpol10.aspx
|
|||
|
|||
|
|||
// jRead.h
|
|||
//
|
|||
// see jRead.c for more information
|
|||
//
|
|||
|
|||
// uncomment this if you really want to use double quotes in query strings instead of '
|
|||
//#define JREAD_DOUBLE_QUOTE_IN_QUERY
|
|||
|
|||
//
|
|||
// return dataTypes:
|
|||
#define JREAD_ERROR 0 // general error, eof etc.
|
|||
#define JREAD_OBJECT 1 // "{"
|
|||
#define JREAD_ARRAY 2 // "["
|
|||
#define JREAD_STRING 3 // "string"
|
|||
#define JREAD_NUMBER 4 // number (may be -ve) int or float
|
|||
#define JREAD_BOOL 5 // true or false
|
|||
#define JREAD_NULL 6 // null
|
|||
#define JREAD_KEY 7 // object "key"
|
|||
// internal values:
|
|||
#define JREAD_COLON 8 // ":"
|
|||
#define JREAD_EOL 9 // end of input string (ptr at '\0')
|
|||
#define JREAD_COMMA 10 // ","
|
|||
#define JREAD_EOBJECT 11 // "}"
|
|||
#define JREAD_EARRAY 12 // "]"
|
|||
#define JREAD_QPARAM 13 // "*" query string parameter
|
|||
|
|||
//------------------------------------------------------
|
|||
// jReadElement
|
|||
// - structure to return JSON elements
|
|||
// - error=0 for valid returns
|
|||
//
|
|||
// *NOTES*
|
|||
// the returned pValue pointer points into the passed JSON
|
|||
// string returns are not '\0' terminated.
|
|||
// bytelen specifies the length of the returned data pointed to by pValue
|
|||
//
|
|||
struct jReadElement{ |
|||
int dataType; // one of JREAD_...
|
|||
int elements; // number of elements (e.g. elements in array or object)
|
|||
int bytelen; // byte length of element (e.g. length of string, array text "[ ... ]" etc.)
|
|||
void * pValue; // pointer to value string in JSON text
|
|||
int error; // error value if dataType == JREAD_ERROR
|
|||
}; |
|||
|
|||
//------------------------------------------------------
|
|||
// The JSON reader function
|
|||
//
|
|||
// - reads a '\0'-terminated JSON text string from pJson
|
|||
// - traverses the JSON according to the pQuery string
|
|||
// - returns the result value in pResult
|
|||
//
|
|||
// returns: pointer into pJson after the queried value
|
|||
//
|
|||
// e.g.
|
|||
// With JSON like: "{ ..., "key":"value", ... }"
|
|||
//
|
|||
// jRead( pJson, "{'key'", &result );
|
|||
// returns with:
|
|||
// result.dataType= JREAD_STRING, result.pValue->'value', result.bytelen=5
|
|||
//
|
|||
char * jRead( char *pJson, char *pQuery, struct jReadElement *pResult ); |
|||
|
|||
// version of jRead which allows one or more queryParam integers to be substituted
|
|||
// for array or object indexes marked by a '*' in the query
|
|||
//
|
|||
// e.g. jReadParam( pJson, "[*", &resultElement, &arrayIndex );
|
|||
//
|
|||
// *!* CAUTION *!*
|
|||
// You can supply an array of integers which are indexed for each '*' in pQuery
|
|||
// however, horrid things will happen if you don't supply enough parameters
|
|||
//
|
|||
char * jReadParam( char *pJson, char *pQuery, struct jReadElement *pResult, int *queryParams ); |
|||
|
|||
// Array Stepping function
|
|||
// - assumes pJsonArray is JSON source of an array "[ ... ]"
|
|||
// - returns next element of the array in pResult
|
|||
// - returns pointer to end of element, to be passed to next call of jReadArrayStep()
|
|||
// - if end of array is encountered, pResult->error = 13 "End of array found"
|
|||
//
|
|||
// e.g.
|
|||
// With JSON like: "{ ... "arrayInObject":[ elem1,elem2,... ], ... }"
|
|||
//
|
|||
// pJson= jRead( pJson, "{'arrayInObject'", &theArray );
|
|||
// if( theArray.dataType == JREAD_ARRAY )
|
|||
// {
|
|||
// char *pArray= (char *)theArray.pValue;
|
|||
// jReadElement arrayElement;
|
|||
// int index;
|
|||
// for( index=0; index < theArray.elements; index++ )
|
|||
// {
|
|||
// pArray= jReadArrayStep( pArray, &arrayElement );
|
|||
// ...
|
|||
//
|
|||
// Note: this significantly speeds up traversing arrays.
|
|||
//
|
|||
char *jReadArrayStep( char *pJsonArray, struct jReadElement *pResult ); |
|||
|
|||
|
|||
#define EXPORT_OPTIONAL_FUNCTIONS |
|||
#ifdef EXPORT_OPTIONAL_FUNCTIONS |
|||
|
|||
//------------------------------------------------------
|
|||
// Optional Helper Functions
|
|||
//
|
|||
long jRead_long( char *pJson, char *pQuery, int *queryParams ); |
|||
int jRead_int( char *pJson, char *pQuery, int *queryParams ); |
|||
double jRead_double( char *pJson, char *pQuery, int *queryParams ); |
|||
int jRead_string( char *pJson, char *pQuery, char *pDest, int destlen, int *queryParams ); |
|||
|
|||
//------------------------------------------------------
|
|||
// Optional String output Functions
|
|||
//
|
|||
char *jReadTypeToString( int dataType ); // string describes dataType
|
|||
char * jReadErrorToString( int error ); // string descibes error code
|
|||
|
|||
//------------------------------------------------------
|
|||
// Other jRead utilities which may be useful...
|
|||
//
|
|||
char * jRead_atoi( char *p, unsigned int *result ); // string to unsigned int
|
|||
char * jRead_atol( char *p, long *result ); // string to signed long
|
|||
char * jRead_atof( char *p, double *result); // string to double (does not do exponents)
|
|||
int jReadStrcmp( struct jReadElement *j1, struct jReadElement *j2 ); // compare STRING elements
|
|||
|
|||
// copy element to '\0'-terminated buffer
|
|||
char * jRead_strcpy( char *destBuffer, int destLength, struct jReadElement *pElement ); |
|||
|
|||
#endif |
|||
// end of jRead.h
|
@ -0,0 +1,566 @@ |
|||
//Author: Tony Wilk
|
|||
//Source: https://www.codeproject.com/Articles/887604/jWrite-A-Really-Simple-JSON-Writer-in-C
|
|||
//LICENSE: The Code Project Open License (CPOL) 1.02
|
|||
//see provided file ICENSE_jReadJWrite.html or https://www.codeproject.com/info/cpol10.aspx
|
|||
|
|||
//
|
|||
// jWrite.c version 1v2
|
|||
//
|
|||
// A *really* simple JSON writer in C
|
|||
//
|
|||
// see: jWrite.h for info
|
|||
//
|
|||
// TonyWilk, Mar 2015
|
|||
//
|
|||
#define _CRT_SECURE_NO_WARNINGS // stop complaining about deprecated functions
|
|||
|
|||
#include <stddef.h> |
|||
#include <stdio.h> |
|||
#include <string.h> // memset() |
|||
|
|||
#include "jWrite.h" |
|||
|
|||
//#include <stdint.h> // definintion of uint32_t, int32_t
|
|||
typedef unsigned int uint32_t; |
|||
typedef int int32_t; |
|||
|
|||
|
|||
// the jWrite functions take the above jWriteControl structure pointer
|
|||
// to maintain state while writing a JSON string.
|
|||
//
|
|||
// You can opt to use a single global instance of a jWriteControl structure
|
|||
// which simplifies the function parameters or to supply your own structure
|
|||
//
|
|||
#ifdef JW_GLOBAL_CONTROL_STRUCT |
|||
struct jWriteControl g_jWriteControl; // global control struct
|
|||
#define JWC_DECL // function parameter decl is empty
|
|||
#define JWC_DECL0 |
|||
#define JWC(x) g_jWriteControl.x // functions access global
|
|||
#define JWC_PARAM // pointer to struct is empty
|
|||
#define JWC_PARAM0 |
|||
#else |
|||
#define JWC_DECL struct jWriteControl *jwc, // function parameter is ptr to control struct
|
|||
#define JWC_DECL0 struct jWriteControl *jwc // function parameter, no params
|
|||
#define JWC(x) jwc->x // functions use pointer
|
|||
#define JWC_PARAM jwc, // pointer to stuct
|
|||
#define JWC_PARAM0 jwc // pointer to stuct, no params
|
|||
#endif |
|||
|
|||
//------------------------------------------
|
|||
// Internal functions
|
|||
//
|
|||
void jwPutch( JWC_DECL char c ); |
|||
void jwPutstr( JWC_DECL char *str ); |
|||
void jwPutraw( JWC_DECL char *str ); |
|||
void modp_itoa10(int32_t value, char* str); |
|||
void modp_dtoa2(double value, char* str, int prec); |
|||
void jwPretty( JWC_DECL0 ); |
|||
enum jwNodeType jwPop( JWC_DECL0 ); |
|||
void jwPush( JWC_DECL enum jwNodeType nodeType ); |
|||
|
|||
|
|||
//------------------------------------------
|
|||
// jwOpen
|
|||
// - open writing of JSON starting with rootType = JW_OBJECT or JW_ARRAY
|
|||
// - initialise with user string buffer of length buflen
|
|||
// - isPretty=JW_PRETTY adds \n and spaces to prettify output (else JW_COMPACT)
|
|||
//
|
|||
void jwOpen( JWC_DECL char *buffer, unsigned int buflen, |
|||
enum jwNodeType rootType, int isPretty ) |
|||
{ |
|||
memset( buffer, 0, buflen ); // zap the whole destination buffer
|
|||
JWC(buffer)= buffer; |
|||
JWC(buflen)= buflen; |
|||
JWC(bufp)= buffer; |
|||
JWC(nodeStack)[0].nodeType= rootType; |
|||
JWC(nodeStack)[0].elementNo= 0; |
|||
JWC(stackpos)=0; |
|||
JWC(error)= JWRITE_OK; |
|||
JWC(callNo)= 1; |
|||
JWC(isPretty)= isPretty; |
|||
jwPutch( JWC_PARAM (rootType==JW_OBJECT) ? '{' : '[' ); |
|||
} |
|||
|
|||
//------------------------------------------
|
|||
// jwClose
|
|||
// - closes the root JSON object started by jwOpen()
|
|||
// - returns error code
|
|||
//
|
|||
int jwClose( JWC_DECL0 ) |
|||
{ |
|||
if( JWC(error) == JWRITE_OK ) |
|||
{ |
|||
if( JWC(stackpos) == 0 ) |
|||
{ |
|||
enum jwNodeType node= JWC(nodeStack)[0].nodeType; |
|||
if( JWC(isPretty) ) |
|||
jwPutch( JWC_PARAM '\n' ); |
|||
jwPutch( JWC_PARAM (node == JW_OBJECT) ? '}' : ']'); |
|||
}else{ |
|||
JWC(error)= JWRITE_NEST_ERROR; // nesting error, not all objects closed when jwClose() called
|
|||
} |
|||
} |
|||
return JWC(error); |
|||
} |
|||
|
|||
//------------------------------------------
|
|||
// End the current array/object
|
|||
//
|
|||
int jwEnd( JWC_DECL0 ) |
|||
{ |
|||
if( JWC(error) == JWRITE_OK ) |
|||
{ |
|||
enum jwNodeType node; |
|||
int lastElemNo= JWC(nodeStack)[JWC(stackpos)].elementNo; |
|||
node= jwPop( JWC_PARAM0 ); |
|||
if( lastElemNo > 0 ) |
|||
jwPretty( JWC_PARAM0 ); |
|||
jwPutch( JWC_PARAM (node == JW_OBJECT) ? '}' : ']'); |
|||
} |
|||
return JWC(error); |
|||
} |
|||
|
|||
|
|||
//------------------------------------------
|
|||
// jwErrorPos
|
|||
// - Returns position of error: the nth call to a jWrite function
|
|||
//
|
|||
int jwErrorPos( JWC_DECL0 ) |
|||
{ |
|||
return JWC(callNo); |
|||
} |
|||
|
|||
|
|||
//------------------------------------------
|
|||
// Object insert functions
|
|||
//
|
|||
int _jwObj( JWC_DECL char *key ); |
|||
|
|||
// put raw string to object (i.e. contents of rawtext without quotes)
|
|||
//
|
|||
void jwObj_raw( JWC_DECL char *key, char *rawtext ) |
|||
{ |
|||
if(_jwObj( JWC_PARAM key ) == JWRITE_OK) |
|||
jwPutraw( JWC_PARAM rawtext); |
|||
} |
|||
|
|||
// put "quoted" string to object
|
|||
//
|
|||
void jwObj_string( JWC_DECL char *key, char *value ) |
|||
{ |
|||
if(_jwObj( JWC_PARAM key ) == JWRITE_OK) |
|||
jwPutstr( JWC_PARAM value ); |
|||
} |
|||
|
|||
void jwObj_int( JWC_DECL char *key, int value ) |
|||
{ |
|||
modp_itoa10( value, JWC(tmpbuf) ); |
|||
jwObj_raw( JWC_PARAM key, JWC(tmpbuf) ); |
|||
} |
|||
|
|||
void jwObj_double( JWC_DECL char *key, double value ) |
|||
{ |
|||
modp_dtoa2( value, JWC(tmpbuf), 6 ); |
|||
jwObj_raw( JWC_PARAM key, JWC(tmpbuf) ); |
|||
} |
|||
|
|||
void jwObj_bool( JWC_DECL char *key, int oneOrZero ) |
|||
{ |
|||
jwObj_raw( JWC_PARAM key, (oneOrZero) ? "true" : "false" ); |
|||
} |
|||
|
|||
void jwObj_null( JWC_DECL char *key ) |
|||
{ |
|||
jwObj_raw( JWC_PARAM key, "null" ); |
|||
} |
|||
|
|||
// put Object in Object
|
|||
//
|
|||
void jwObj_object( JWC_DECL char *key ) |
|||
{ |
|||
if(_jwObj( JWC_PARAM key ) == JWRITE_OK) |
|||
{ |
|||
jwPutch( JWC_PARAM '{' ); |
|||
jwPush( JWC_PARAM JW_OBJECT ); |
|||
} |
|||
} |
|||
|
|||
// put Array in Object
|
|||
//
|
|||
void jwObj_array( JWC_DECL char *key ) |
|||
{ |
|||
if(_jwObj( JWC_PARAM key ) == JWRITE_OK) |
|||
{ |
|||
jwPutch( JWC_PARAM '[' ); |
|||
jwPush( JWC_PARAM JW_ARRAY ); |
|||
} |
|||
} |
|||
|
|||
//------------------------------------------
|
|||
// Array insert functions
|
|||
//
|
|||
int _jwArr( JWC_DECL0 ); |
|||
|
|||
// put raw string to array (i.e. contents of rawtext without quotes)
|
|||
//
|
|||
void jwArr_raw( JWC_DECL char *rawtext ) |
|||
{ |
|||
if(_jwArr( JWC_PARAM0 ) == JWRITE_OK) |
|||
jwPutraw( JWC_PARAM rawtext); |
|||
} |
|||
|
|||
// put "quoted" string to array
|
|||
//
|
|||
void jwArr_string( JWC_DECL char *value ) |
|||
{ |
|||
if(_jwArr( JWC_PARAM0 ) == JWRITE_OK) |
|||
jwPutstr( JWC_PARAM value ); |
|||
} |
|||
|
|||
void jwArr_int( JWC_DECL int value ) |
|||
{ |
|||
modp_itoa10( value, JWC(tmpbuf) ); |
|||
jwArr_raw( JWC_PARAM JWC(tmpbuf) ); |
|||
} |
|||
|
|||
void jwArr_double( JWC_DECL double value ) |
|||
{ |
|||
modp_dtoa2( value, JWC(tmpbuf), 6 ); |
|||
jwArr_raw( JWC_PARAM JWC(tmpbuf) ); |
|||
} |
|||
|
|||
void jwArr_bool( JWC_DECL int oneOrZero ) |
|||
{ |
|||
jwArr_raw( JWC_PARAM (oneOrZero) ? "true" : "false" ); |
|||
} |
|||
|
|||
void jwArr_null( JWC_DECL0 ) |
|||
{ |
|||
jwArr_raw( JWC_PARAM "null" ); |
|||
} |
|||
|
|||
void jwArr_object( JWC_DECL0 ) |
|||
{ |
|||
if(_jwArr( JWC_PARAM0 ) == JWRITE_OK) |
|||
{ |
|||
jwPutch( JWC_PARAM '{' ); |
|||
jwPush( JWC_PARAM JW_OBJECT ); |
|||
} |
|||
} |
|||
|
|||
void jwArr_array( JWC_DECL0 ) |
|||
{ |
|||
if(_jwArr( JWC_PARAM0 ) == JWRITE_OK) |
|||
{ |
|||
jwPutch( JWC_PARAM '[' ); |
|||
jwPush( JWC_PARAM JW_ARRAY ); |
|||
} |
|||
} |
|||
|
|||
|
|||
//------------------------------------------
|
|||
// jwErrorToString
|
|||
// - returns string describing error code
|
|||
//
|
|||
char *jwErrorToString( int err ) |
|||
{ |
|||
switch( err ) |
|||
{ |
|||
case JWRITE_OK: return "OK"; |
|||
case JWRITE_BUF_FULL: return "output buffer full"; |
|||
case JWRITE_NOT_ARRAY: return "tried to write Array value into Object"; |
|||
case JWRITE_NOT_OBJECT: return "tried to write Object key/value into Array"; |
|||
case JWRITE_STACK_FULL: return "array/object nesting > JWRITE_STACK_DEPTH"; |
|||
case JWRITE_STACK_EMPTY:return "stack underflow error (too many 'end's)"; |
|||
case JWRITE_NEST_ERROR: return "nesting error, not all objects closed when jwClose() called"; |
|||
} |
|||
return "Unknown error"; |
|||
} |
|||
|
|||
//============================================================================
|
|||
// Internal functions
|
|||
//
|
|||
void jwPretty( JWC_DECL0 ) |
|||
{ |
|||
int i; |
|||
if( JWC(isPretty) ) |
|||
{ |
|||
jwPutch( JWC_PARAM '\n' ); |
|||
for( i=0; i<JWC(stackpos)+1; i++ ) |
|||
jwPutraw( JWC_PARAM " " ); |
|||
} |
|||
} |
|||
|
|||
// Push / Pop node stack
|
|||
//
|
|||
void jwPush( JWC_DECL enum jwNodeType nodeType ) |
|||
{ |
|||
if( (JWC(stackpos)+1) >= JWRITE_STACK_DEPTH ) |
|||
JWC(error)= JWRITE_STACK_FULL; // array/object nesting > JWRITE_STACK_DEPTH
|
|||
else |
|||
{ |
|||
JWC(nodeStack[++JWC(stackpos)]).nodeType= nodeType; |
|||
JWC(nodeStack[JWC(stackpos)]).elementNo= 0; |
|||
} |
|||
} |
|||
|
|||
enum jwNodeType jwPop( JWC_DECL0 ) |
|||
{ |
|||
enum jwNodeType retval= JWC(nodeStack[JWC(stackpos)]).nodeType; |
|||
if( JWC(stackpos) == 0 ) |
|||
JWC(error)= JWRITE_STACK_EMPTY; // stack underflow error (too many 'end's)
|
|||
else |
|||
JWC(stackpos)--; |
|||
return retval; |
|||
} |
|||
|
|||
void jwPutch( JWC_DECL char c ) |
|||
{ |
|||
if( (unsigned int)(JWC(bufp) - JWC(buffer)) >= JWC(buflen) ) |
|||
{ |
|||
JWC(error)= JWRITE_BUF_FULL; |
|||
}else{ |
|||
*JWC(bufp)++ = c; |
|||
} |
|||
} |
|||
|
|||
// put string enclosed in quotes
|
|||
//
|
|||
void jwPutstr( JWC_DECL char *str ) |
|||
{ |
|||
jwPutch( JWC_PARAM '\"' ); |
|||
while( *str != '\0' ) |
|||
jwPutch( JWC_PARAM *str++ ); |
|||
jwPutch( JWC_PARAM '\"' ); |
|||
} |
|||
|
|||
// put raw string
|
|||
//
|
|||
void jwPutraw( JWC_DECL char *str ) |
|||
{ |
|||
while( *str != '\0' ) |
|||
jwPutch( JWC_PARAM *str++ ); |
|||
} |
|||
|
|||
|
|||
// *common Object function*
|
|||
// - checks error
|
|||
// - checks current node is OBJECT
|
|||
// - adds comma if reqd
|
|||
// - adds "key" :
|
|||
//
|
|||
int _jwObj( JWC_DECL char *key ) |
|||
{ |
|||
if(JWC(error) == JWRITE_OK) |
|||
{ |
|||
JWC(callNo)++; |
|||
if( JWC(nodeStack)[JWC(stackpos)].nodeType != JW_OBJECT ) |
|||
JWC(error)= JWRITE_NOT_OBJECT; // tried to write Object key/value into Array
|
|||
else if( JWC(nodeStack)[JWC(stackpos)].elementNo++ > 0 ) |
|||
jwPutch( JWC_PARAM ',' ); |
|||
jwPretty( JWC_PARAM0 ); |
|||
jwPutstr( JWC_PARAM key ); |
|||
jwPutch( JWC_PARAM ':' ); |
|||
if( JWC(isPretty) ) |
|||
jwPutch( JWC_PARAM ' ' ); |
|||
} |
|||
return JWC(error); |
|||
} |
|||
|
|||
// *common Array function*
|
|||
// - checks error
|
|||
// - checks current node is ARRAY
|
|||
// - adds comma if reqd
|
|||
//
|
|||
int _jwArr( JWC_DECL0 ) |
|||
{ |
|||
if(JWC(error) == JWRITE_OK) |
|||
{ |
|||
JWC(callNo)++; |
|||
if( JWC(nodeStack)[JWC(stackpos)].nodeType != JW_ARRAY ) |
|||
JWC(error)= JWRITE_NOT_ARRAY; // tried to write array value into Object
|
|||
else if( JWC(nodeStack)[JWC(stackpos)].elementNo++ > 0 ) |
|||
jwPutch( JWC_PARAM ',' ); |
|||
jwPretty( JWC_PARAM0 ); |
|||
} |
|||
return JWC(error); |
|||
} |
|||
|
|||
//=================================================================
|
|||
//
|
|||
// modp value-to-string functions
|
|||
// - modified for C89
|
|||
//
|
|||
// We use these functions as they are a lot faster than sprintf()
|
|||
//
|
|||
// Origin of these routines:
|
|||
/*
|
|||
* <pre> |
|||
* Copyright © 2007, Nick Galbreath -- nickg [at] modp [dot] com |
|||
* All rights reserved. |
|||
* http://code.google.com/p/stringencoders/
|
|||
* Released under the bsd license. |
|||
* </pre> |
|||
*/ |
|||
|
|||
static void strreverse(char* begin, char* end) |
|||
{ |
|||
char aux; |
|||
while (end > begin) |
|||
aux = *end, *end-- = *begin, *begin++ = aux; |
|||
} |
|||
|
|||
/** \brief convert an signed integer to char buffer
|
|||
* |
|||
* \param[in] value |
|||
* \param[out] buf the output buffer. Should be 16 chars or more. |
|||
*/ |
|||
void modp_itoa10(int32_t value, char* str) |
|||
{ |
|||
char* wstr=str; |
|||
// Take care of sign
|
|||
unsigned int uvalue = (value < 0) ? -value : value; |
|||
// Conversion. Number is reversed.
|
|||
do *wstr++ = (char)(48 + (uvalue % 10)); while(uvalue /= 10); |
|||
if (value < 0) *wstr++ = '-'; |
|||
*wstr='\0'; |
|||
|
|||
// Reverse string
|
|||
strreverse(str,wstr-1); |
|||
} |
|||
|
|||
/**
|
|||
* Powers of 10 |
|||
* 10^0 to 10^9 |
|||
*/ |
|||
static const double pow10[] = {1, 10, 100, 1000, 10000, 100000, 1000000, |
|||
10000000, 100000000, 1000000000}; |
|||
|
|||
/** \brief convert a floating point number to char buffer with a
|
|||
* variable-precision format, and no trailing zeros |
|||
* |
|||
* This is similar to "%.[0-9]f" in the printf style, except it will |
|||
* NOT include trailing zeros after the decimal point. This type |
|||
* of format oddly does not exists with printf. |
|||
* |
|||
* If the input value is greater than 1<<31, then the output format |
|||
* will be switched exponential format. |
|||
* |
|||
* \param[in] value |
|||
* \param[out] buf The allocated output buffer. Should be 32 chars or more. |
|||
* \param[in] precision Number of digits to the right of the decimal point. |
|||
* Can only be 0-9. |
|||
*/ |
|||
void modp_dtoa2(double value, char* str, int prec) |
|||
{ |
|||
/* if input is larger than thres_max, revert to exponential */ |
|||
const double thres_max = (double)(0x7FFFFFFF); |
|||
int count; |
|||
double diff = 0.0; |
|||
char* wstr = str; |
|||
int neg= 0; |
|||
int whole; |
|||
double tmp; |
|||
uint32_t frac; |
|||
|
|||
/* Hacky test for NaN
|
|||
* under -fast-math this won't work, but then you also won't |
|||
* have correct nan values anyways. The alternative is |
|||
* to link with libmath (bad) or hack IEEE double bits (bad) |
|||
*/ |
|||
if (! (value == value)) { |
|||
str[0] = 'n'; str[1] = 'a'; str[2] = 'n'; str[3] = '\0'; |
|||
return; |
|||
} |
|||
|
|||
if (prec < 0) { |
|||
prec = 0; |
|||
} else if (prec > 9) { |
|||
/* precision of >= 10 can lead to overflow errors */ |
|||
prec = 9; |
|||
} |
|||
|
|||
/* we'll work in positive values and deal with the
|
|||
negative sign issue later */ |
|||
if (value < 0) { |
|||
neg = 1; |
|||
value = -value; |
|||
} |
|||
|
|||
|
|||
whole = (int) value; |
|||
tmp = (value - whole) * pow10[prec]; |
|||
frac = (uint32_t)(tmp); |
|||
diff = tmp - frac; |
|||
|
|||
if (diff > 0.5) { |
|||
++frac; |
|||
/* handle rollover, e.g. case 0.99 with prec 1 is 1.0 */ |
|||
if (frac >= pow10[prec]) { |
|||
frac = 0; |
|||
++whole; |
|||
} |
|||
} else if (diff == 0.5 && ((frac == 0) || (frac & 1))) { |
|||
/* if halfway, round up if odd, OR
|
|||
if last digit is 0. That last part is strange */ |
|||
++frac; |
|||
} |
|||
|
|||
/* for very large numbers switch back to native sprintf for exponentials.
|
|||
anyone want to write code to replace this? */ |
|||
/*
|
|||
normal printf behavior is to print EVERY whole number digit |
|||
which can be 100s of characters overflowing your buffers == bad |
|||
*/ |
|||
if (value > thres_max) { |
|||
sprintf(str, "%e", neg ? -value : value); |
|||
return; |
|||
} |
|||
|
|||
if (prec == 0) { |
|||
diff = value - whole; |
|||
if (diff > 0.5) { |
|||
/* greater than 0.5, round up, e.g. 1.6 -> 2 */ |
|||
++whole; |
|||
} else if (diff == 0.5 && (whole & 1)) { |
|||
/* exactly 0.5 and ODD, then round up */ |
|||
/* 1.5 -> 2, but 2.5 -> 2 */ |
|||
++whole; |
|||
} |
|||
|
|||
//vvvvvvvvvvvvvvvvvvv Diff from modp_dto2
|
|||
} else if (frac) { |
|||
count = prec; |
|||
// now do fractional part, as an unsigned number
|
|||
// we know it is not 0 but we can have leading zeros, these
|
|||
// should be removed
|
|||
while (!(frac % 10)) { |
|||
--count; |
|||
frac /= 10; |
|||
} |
|||
//^^^^^^^^^^^^^^^^^^^ Diff from modp_dto2
|
|||
|
|||
// now do fractional part, as an unsigned number
|
|||
do { |
|||
--count; |
|||
*wstr++ = (char)(48 + (frac % 10)); |
|||
} while (frac /= 10); |
|||
// add extra 0s
|
|||
while (count-- > 0) *wstr++ = '0'; |
|||
// add decimal
|
|||
*wstr++ = '.'; |
|||
} |
|||
|
|||
// do whole part
|
|||
// Take care of sign
|
|||
// Conversion. Number is reversed.
|
|||
do *wstr++ = (char)(48 + (whole % 10)); while (whole /= 10); |
|||
if (neg) { |
|||
*wstr++ = '-'; |
|||
} |
|||
*wstr='\0'; |
|||
strreverse(str, wstr-1); |
|||
} |
|||
//=================================================================
|
|||
|
|||
/* end of jWrite.c */ |
@ -0,0 +1,218 @@ |
|||
//Author: Tony Wilk
|
|||
//Source: https://www.codeproject.com/Articles/887604/jWrite-A-Really-Simple-JSON-Writer-in-C
|
|||
//LICENSE: The Code Project Open License (CPOL) 1.02
|
|||
//see provided file ICENSE_jReadJWrite.html or https://www.codeproject.com/info/cpol10.aspx
|
|||
|
|||
//
|
|||
// jWrite.h
|
|||
//
|
|||
// A *really* simple JSON writer in C (C89)
|
|||
// - a collection of functions to generate JSON semi-automatically
|
|||
//
|
|||
// The idea is to simplify writing native C values into a JSON string and
|
|||
// to provide some error trapping to ensure that the result is valid JSON.
|
|||
//
|
|||
// Example:
|
|||
// jwOpen( buffer, buflen, JW_OBJECT, JW_PRETTY ); // open root node as object
|
|||
// jwObj_string( "key", "value" );
|
|||
// jwObj_int( "int", 1 );
|
|||
// jwObj_array( "anArray");
|
|||
// jwArr_int( 0 );
|
|||
// jwArr_int( 1 );
|
|||
// jwArr_int( 2 );
|
|||
// jwEnd();
|
|||
// err= jwClose(); // close root object
|
|||
//
|
|||
// results in:
|
|||
//
|
|||
// {
|
|||
// "key": "value",
|
|||
// "int": 1,
|
|||
// "anArray": [
|
|||
// 0,
|
|||
// 1,
|
|||
// 2
|
|||
// ]
|
|||
// }
|
|||
//
|
|||
// Note that jWrite handles string quoting and getting commas in the right place.
|
|||
// If the sequence of calls is incorrect
|
|||
// e.g.
|
|||
// jwOpen( buffer, buflen, JW_OBJECT, 1 );
|
|||
// jwObj_string( "key", "value" );
|
|||
// jwArr_int( 0 );
|
|||
// ...
|
|||
//
|
|||
// then the error code returned from jwClose() would indicate that you attempted to
|
|||
// put an array element into an object (instead of a key:value pair)
|
|||
// To locate the error, the supplied buffer has the JSON created upto the error point
|
|||
// and a call to jwErrorPos() would return the function call at which the error occurred
|
|||
// - in this case 3, the 3rd function call "jwArr_int(0)" is not correct at this point.
|
|||
//
|
|||
// The root JSON type can be JW_OBJECT or JW_ARRAY.
|
|||
//
|
|||
// For more information on each function, see the prototypes below.
|
|||
//
|
|||
//
|
|||
// GLOBAL vs. Application-Supplied Control Structure
|
|||
// -------------------------------------------------
|
|||
// jWrite requires a jWriteControl structure to save the internal state.
|
|||
// For many applications it is much simpler for this to be a global variable as
|
|||
// used by the above examples.
|
|||
//
|
|||
// To use multiple instances of jWrite, an application has to supply unique instances
|
|||
// of jWriteControl structures.
|
|||
//
|
|||
// This feature is enabled by commenting out the definition of JW_GLOBAL_CONTROL_STRUCT
|
|||
//
|
|||
// All the jWrite functions then take an additional parameter: a ptr to the structure
|
|||
// e.g.
|
|||
// struct jWriteControl jwc;
|
|||
//
|
|||
// jwOpen( &jwc, buffer, buflen, JW_OBJECT, 1 );
|
|||
// jwObj_string( &jwc, "key", "value" );
|
|||
// jwObj_int( &jwc, "int", 1 );
|
|||
// jwObj_array( &jwc, "anArray");
|
|||
// jwArr_int( &jwc, 0 );
|
|||
// jwArr_int( &jwc, 1 );
|
|||
// jwArr_int( &jwc, 2 );
|
|||
// jwEnd( &jwc );
|
|||
// err= jwClose( &jwc );
|
|||
//
|
|||
// - which is more flexible, but a pain to type in !
|
|||
//
|
|||
// TonyWilk, Mar 2015
|
|||
//
|
|||
//
|
|||
#define JW_GLOBAL_CONTROL_STRUCT // <--- comment this out to use applic-supplied jWriteControl
|
|||
|
|||
#define JWRITE_STACK_DEPTH 32 // max nesting depth of objects/arrays
|
|||
|
|||
#define JW_COMPACT 0 // output string control for jwOpen()
|
|||
#define JW_PRETTY 1 // pretty adds \n and indentation
|
|||
|
|||
enum jwNodeType{ |
|||
JW_OBJECT= 1, |
|||
JW_ARRAY |
|||
}; |
|||
|
|||
struct jwNodeStack{ |
|||
enum jwNodeType nodeType; |
|||
int elementNo; |
|||
}; |
|||
|
|||
struct jWriteControl{ |
|||
char *buffer; // pointer to application's buffer
|
|||
unsigned int buflen; // length of buffer
|
|||
char *bufp; // current write position in buffer
|
|||
char tmpbuf[32]; // local buffer for int/double convertions
|
|||
int error; // error code
|
|||
int callNo; // API call on which error occurred
|
|||
struct jwNodeStack nodeStack[JWRITE_STACK_DEPTH]; // stack of array/object nodes
|
|||
int stackpos; |
|||
int isPretty; // 1= pretty output (inserts \n and spaces)
|
|||
}; |
|||
|
|||
// Error Codes
|
|||
// -----------
|
|||
#define JWRITE_OK 0 |
|||
#define JWRITE_BUF_FULL 1 // output buffer full
|
|||
#define JWRITE_NOT_ARRAY 2 // tried to write Array value into Object
|
|||
#define JWRITE_NOT_OBJECT 3 // tried to write Object key/value into Array
|
|||
#define JWRITE_STACK_FULL 4 // array/object nesting > JWRITE_STACK_DEPTH
|
|||
#define JWRITE_STACK_EMPTY 5 // stack underflow error (too many 'end's)
|
|||
#define JWRITE_NEST_ERROR 6 // nesting error, not all objects closed when jwClose() called
|
|||
|
|||
|
|||
// API functions
|
|||
// -------------
|
|||
|
|||
// Returns '\0'-termianted string describing the error (as returned by jwClose())
|
|||
//
|
|||
char *jwErrorToString( int err ); |
|||
|
|||
|
|||
#ifdef JW_GLOBAL_CONTROL_STRUCT /* USING GLOBAL g_jWriteControl */ |
|||
|
|||
// jwOpen
|
|||
// - initialises jWrite with the application supplied 'buffer' of length 'buflen'
|
|||
// in operation, the buffer will always contain a valid '\0'-terminated string
|
|||
// - jWrite will not overrun the buffer (it returns an "output buffer full" error)
|
|||
// - rootType is the base JSON type: JW_OBJECT or JW_ARRAY
|
|||
// - isPretty controls 'prettifying' the output: JW_PRETTY or JW_COMPACT
|
|||
void jwOpen( char *buffer, unsigned int buflen, enum jwNodeType rootType, int isPretty ); |
|||
|
|||
// jwClose
|
|||
// - closes the element opened by jwOpen()
|
|||
// - returns error code (0 = JWRITE_OK)
|
|||
// - after an error, all following jWrite calls are skipped internally
|
|||
// so the error code is for the first error detected
|
|||
int jwClose( ); |
|||
|
|||
// jwErrorPos
|
|||
// - if jwClose returned an error, this function returns the number of the jWrite function call
|
|||
// which caused that error.
|
|||
int jwErrorPos( ); |
|||
|
|||
// Object insertion functions
|
|||
// - used to insert "key":"value" pairs into an object
|
|||
//
|
|||
void jwObj_string( char *key, char *value ); |
|||
void jwObj_int( char *key, int value ); |
|||
void jwObj_double( char *key, double value ); |
|||
void jwObj_bool( char *key, int oneOrZero ); |
|||
void jwObj_null( char *key ); |
|||
void jwObj_object( char *key ); |
|||
void jwObj_array( char *key ); |
|||
|
|||
// Array insertion functions
|
|||
// - used to insert "value" elements into an array
|
|||
//
|
|||
void jwArr_string( char *value ); |
|||
void jwArr_int( int value ); |
|||
void jwArr_double( double value ); |
|||
void jwArr_bool( int oneOrZero ); |
|||
void jwArr_null( ); |
|||
void jwArr_object( ); |
|||
void jwArr_array( ); |
|||
|
|||
// jwEnd
|
|||
// - defines the end of an Object or Array definition
|
|||
int jwEnd( ); |
|||
|
|||
|
|||
// these 'raw' routines write the JSON value as the contents of rawtext
|
|||
// i.e. enclosing quotes are not added
|
|||
// - use if your app. supplies its own value->string functions
|
|||
//
|
|||
void jwObj_raw( char *key, char *rawtext ); |
|||
void jwArr_raw( char *rawtext ); |
|||
|
|||
#else /* JW_GLOBAL_CONTROL_STRUCT not defined */ |
|||
// Same API functions with app-supplied control struct option
|
|||
//
|
|||
void jwOpen( struct jWriteControl *jwc, char *buffer, unsigned int buflen, enum jwNodeType rootType, int isPretty ); |
|||
int jwClose( struct jWriteControl *jwc ); |
|||
int jwErrorPos( struct jWriteControl *jwc ); |
|||
void jwObj_string( struct jWriteControl *jwc, char *key, char *value ); |
|||
void jwObj_int( struct jWriteControl *jwc, char *key, int value ); |
|||
void jwObj_double( struct jWriteControl *jwc, char *key, double value ); |
|||
void jwObj_bool( struct jWriteControl *jwc, char *key, int oneOrZero ); |
|||
void jwObj_null( struct jWriteControl *jwc, char *key ); |
|||
void jwObj_object( struct jWriteControl *jwc, char *key ); |
|||
void jwObj_array( struct jWriteControl *jwc, char *key ); |
|||
void jwArr_string( struct jWriteControl *jwc, char *value ); |
|||
void jwArr_int( struct jWriteControl *jwc, int value ); |
|||
void jwArr_double( struct jWriteControl *jwc, double value ); |
|||
void jwArr_bool( struct jWriteControl *jwc, int oneOrZero ); |
|||
void jwArr_null( struct jWriteControl *jwc ); |
|||
void jwArr_object( struct jWriteControl *jwc ); |
|||
void jwArr_array( struct jWriteControl *jwc ); |
|||
int jwEnd( struct jWriteControl *jwc ); |
|||
void jwObj_raw( struct jWriteControl *jwc, char *key, char *rawtext ); |
|||
void jwArr_raw( struct jWriteControl *jwc, char *rawtext ); |
|||
|
|||
#endif /* JW_GLOBAL_CONTROL_STRUCT */ |
|||
|
|||
|
|||
/* end of jWrite.h */ |
@ -0,0 +1,266 @@ |
|||
//Standard lib
|
|||
#include <string.h> //memcpy |
|||
#include <stdio.h> //print error messages |
|||
#include <unistd.h> |
|||
|
|||
|
|||
//Third party
|
|||
#include <jack/jack.h> |
|||
#include <jack/midiport.h> |
|||
#include <jack/ringbuffer.h> |
|||
#include <jack/metadata.h> |
|||
|
|||
//Our own files
|
|||
#include "jackclient.h" |
|||
#include "constants.h" |
|||
#include "programstate.h" |
|||
|
|||
extern ProgramState programState; |
|||
|
|||
static jack_client_t* client; |
|||
//static jack_ringbuffer_t *ringbuffer = NULL;
|
|||
|
|||
jack_ringbuffer_t *allRingbuffers[VIS_PORTS]; |
|||
jack_port_t *allPorts[VIS_PORTS]; |
|||
|
|||
static pthread_mutex_t msg_thread_lock = PTHREAD_MUTEX_INITIALIZER; |
|||
static pthread_cond_t data_ready = PTHREAD_COND_INITIALIZER; |
|||
|
|||
#define RBSIZE 512 |
|||
|
|||
typedef struct { |
|||
uint8_t buffer[128]; |
|||
uint32_t size; |
|||
} midimsg; |
|||
|
|||
|
|||
int process (jack_nframes_t frames, void* arg) { |
|||
void* buffer; |
|||
jack_nframes_t N; |
|||
jack_nframes_t i; |
|||
jack_port_t* port; |
|||
jack_ringbuffer_t* rb=NULL; |
|||
|
|||
for (int portnum=0; portnum<VIS_PORTS; portnum++) { |
|||
port = allPorts[portnum]; |
|||
rb = allRingbuffers[portnum]; |
|||
buffer = jack_port_get_buffer (port, frames); |
|||
//assert (buffer);
|
|||
|
|||
N = jack_midi_get_event_count (buffer); |
|||
for (int i = 0; i < N; ++i) { |
|||
jack_midi_event_t event; |
|||
int r; |
|||
|
|||
r = jack_midi_event_get (&event, buffer, i); |
|||
|
|||
if (r == 0 && jack_ringbuffer_write_space(rb) >= sizeof(midimsg)) { |
|||
midimsg message; |
|||
message.size = event.size; |
|||
memcpy (message.buffer, event.buffer, MAX(sizeof(message.buffer), event.size)); |
|||
jack_ringbuffer_write (rb, (void *) &message, sizeof(midimsg)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
if (pthread_mutex_trylock (&msg_thread_lock) == 0) { |
|||
pthread_cond_signal (&data_ready); |
|||
pthread_mutex_unlock (&msg_thread_lock); |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
static void handleMessage (Note *allGUINotes, int portnum, midimsg* event) { |
|||
if (event->size == 0) { |
|||
return; |
|||
} |
|||
|
|||
uint8_t type = event->buffer[0] & 0xf0; |
|||
uint8_t channel = event->buffer[0] & 0xf; |
|||
Note * note; |
|||
|
|||
switch (type) { |
|||
case 0x90: |
|||
//assert (event->size == 3);
|
|||
//printf (" note on (channel %2d): pitch %3d, velocity %3d\n", channel, event->buffer[1], event->buffer[2]);
|
|||
programState.portActivity[portnum] += 1; |
|||
note = &allGUINotes[portnum*128 + event->buffer[1]]; |
|||
note->active = 1; |
|||
note->countdown = 1.0f; |
|||
note->velocity = event->buffer[2]; |
|||
note->pitch = event->buffer[1]; |
|||
note->port = portnum; |
|||
break; |
|||
case 0x80: |
|||
//assert (event->size == 3);
|
|||
//printf (" note off (channel %2d): pitch %3d, velocity %3d\n", channel, event->buffer[1], event->buffer[2]);
|
|||
programState.portActivity[portnum] -= 1; |
|||
if (programState.portActivity[portnum] < 1) { programState.portActivity[portnum] = 0;} |
|||
note = &allGUINotes[portnum*128 + event->buffer[1]]; |
|||
note->countdown = 0.999f; //GUI will reduce that to 0 on its own as fade-out effect
|
|||
//Do NOT set to inactive. Note-Off triggers a graphical fadeout, only when that is over it is inactive.
|
|||
break; |
|||
//case 0xb0:
|
|||
//assert (event->size == 3);
|
|||
//printf (" control change (channel %2d): controller %3d, value %3d", channel, event->buffer[1], event->buffer[2]);
|
|||
// break;
|
|||
default: |
|||
break; |
|||
} |
|||
} |
|||
|
|||
void mainLoop_processMidiMessages(Note *allGUINotes) { |
|||
|
|||
for (int portnum=0; portnum<VIS_PORTS; portnum++) { |
|||
int mqlen = jack_ringbuffer_read_space (allRingbuffers[portnum]) / sizeof(midimsg); |
|||
for (int i=0; i < mqlen; ++i) { |
|||
size_t j; |
|||
midimsg message; |
|||
jack_ringbuffer_read(allRingbuffers[portnum], (char*) &message, sizeof(midimsg)); |
|||
handleMessage (allGUINotes, portnum, &message); |
|||
} |
|||
} |
|||
pthread_cond_wait (&data_ready, &msg_thread_lock); |
|||
|
|||
} |
|||
|
|||
void mainLoop_collectTimingAndTransportInProgramState() { |
|||
jack_position_t pos; |
|||
jack_transport_state_t state = jack_transport_query(client, &pos); |
|||
|
|||
int totalSeconds = pos.frame / pos.frame_rate; |
|||
int hours = totalSeconds / 3600; //int devision drops decimal
|
|||
int minutes = totalSeconds / 60 % 60; |
|||
int seconds = totalSeconds % 60; |
|||
|
|||
snprintf(programState.clockString, (sizeof(char) * 255), "%i:%02i:%02i ( %is )", hours, minutes, seconds, totalSeconds); |
|||
|
|||
programState.transportRolling = state != JackTransportStopped; |
|||
|
|||
if (pos.beats_per_minute > 1.0d) { //comparing to 0 does not work
|
|||
programState.bpm = pos.beats_per_minute; |
|||
} |
|||
else |
|||
{ |
|||
programState.bpm = 120.0d; |
|||
} |
|||
|
|||
} |
|||
|
|||
|
|||
void ourConnectCallback(jack_port_id_t a, jack_port_id_t b, int connect, void *arg) { |
|||
|
|||
//On each connection change go through our ports and check the situation.
|
|||
//Only the first port is enough for our purposes
|
|||
|
|||
//TODO: optimisation opportunity: keep a list of our port ids and check that first.
|
|||
|
|||
for (int portnum=0; portnum<VIS_PORTS; portnum++) { |
|||
const char** connectedPorts; |
|||
connectedPorts = jack_port_get_connections(allPorts[portnum]); |
|||
|
|||
if (connectedPorts && connectedPorts[0]) |
|||
{ |
|||
//We could use connectedPorts[0] directly as a name, this is the real portname.
|
|||
//However, we try to get a pretty name from Jack Metadata first
|
|||
jack_port_t * externalPort; |
|||
externalPort = jack_port_by_name(client, connectedPorts[0]); |
|||
jack_uuid_t externalId = jack_port_uuid(externalPort); |
|||
char * externalPrettyName; |
|||
char * externalType; |
|||
int returnCode = jack_get_property(externalId, JACK_METADATA_PRETTY_NAME, &externalPrettyName, &externalType); |
|||
if (!returnCode && externalPrettyName) { |
|||
//const char * prettyPortConst = externalPrettyName;
|
|||
//strcpy(prettyPortNameCopy, externalPrettyName); //strcpy is const -> non-const
|
|||
programState.connectedPortNames[portnum] = externalPrettyName; |
|||
//if (externalPrettyName) jack_free(externalPrettyName); //TODO: If I free that the name either vanishes, segfaults, is wrong or garbage or it is always the same. No matter what string copy, conversion etc. I tried
|
|||
if (externalType) jack_free(externalType); |
|||
} |
|||
else { // No Metadata for this port found
|
|||
programState.connectedPortNames[portnum] = connectedPorts[0]; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
programState.connectedPortNames[portnum] = ""; //Reset
|
|||
} |
|||
jack_free(connectedPorts); |
|||
} |
|||
|
|||
} |
|||
|
|||
void createJackClient() { |
|||
|
|||
int errorCode; |
|||
|
|||
client = jack_client_open (programState.name, JackNullOption, NULL); |
|||
if (client == NULL) { |
|||
fprintf (stderr, "Could not create JACK client.\n"); |
|||
exit (EXIT_FAILURE); |
|||
} |
|||
|
|||
jack_set_process_callback (client, process, 0); |
|||
|
|||
for (int portnum=0; portnum<VIS_PORTS;portnum++) { |
|||
allRingbuffers[portnum] = jack_ringbuffer_create (RBSIZE * sizeof(midimsg)); //Communicate between RT and Non-RT
|
|||
char name[64]; |
|||
sprintf(name, "%i", portnum); |
|||
allPorts[portnum] = jack_port_register (client, name, JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0); |
|||
if (allPorts[portnum] == NULL) { |
|||
fprintf (stderr, "Could not register port.\n"); |
|||
exit (EXIT_FAILURE); |
|||
} |
|||
} |
|||
|
|||
//Callbacks must be registered before activating the client
|
|||
errorCode = jack_set_port_connect_callback(client, ourConnectCallback, 0); |
|||
if (errorCode != 0) { |
|||
fprintf (stderr, "Could not register port registration callback.\n"); |
|||
exit (EXIT_FAILURE); |
|||
} |
|||
|
|||
//Now activate
|
|||
errorCode = jack_activate (client); |
|||
if (errorCode != 0) { |
|||
fprintf (stderr, "Could not activate client.\n"); |
|||
exit (EXIT_FAILURE); |
|||
} |
|||
|
|||
|
|||
} |
|||
|
|||
void closeJackClient() { |
|||
pthread_mutex_unlock (&msg_thread_lock); |
|||
jack_deactivate (client); |
|||
jack_client_close (client); |
|||
for (int portnum=0; portnum<VIS_PORTS;portnum++) { |
|||
jack_ringbuffer_free (allRingbuffers[portnum]); |
|||
} |
|||
printf("Jack Client closed succesfully\n"); |
|||
} |
|||
|
|||
void seekToStart() { |
|||
jack_transport_locate(client, 0); |
|||
} |
|||
|
|||
void toggleTransport() { |
|||
//This is only the active toggle from our side. No reaction to external transport
|
|||
|
|||
//Called from non-rt thread
|
|||
jack_position_t pos; |
|||
jack_transport_state_t state = jack_transport_query(client, &pos); |
|||
|
|||
if (state == JackTransportStopped) { |
|||
jack_transport_start(client); |
|||
} |
|||
else if (state == JackTransportStarting) { |
|||
//Waiting for sync ready
|
|||
} |
|||
else { |
|||
jack_transport_stop(client); |
|||
} |
|||
} |
|||
|
|||
|
@ -0,0 +1,19 @@ |
|||
#ifndef JACKCLIENT_H |
|||
#define JACKCLIENT_H |
|||
|
|||
#include "constants.h" //for Note struct |
|||
#include <jack/jack.h> |
|||
|
|||
|
|||
int process (jack_nframes_t frames, void* arg); |
|||
|
|||
void createJackClient(); |
|||
void closeJackClient(); |
|||
void mainLoop_processMidiMessages(Note *allGUINotes); |
|||
void toggleTransport(); |
|||
void seekToStart(); |
|||
//char * positionAsClock();
|
|||
void mainLoop_collectTimingAndTransportInProgramState(); |
|||
|
|||
|
|||
#endif // not defined JACKCLIENT_H
|
@ -0,0 +1,405 @@ |
|||
//Standard lib
|
|||
#include <stdio.h> |
|||
#include <signal.h> |
|||
|
|||
//Third party
|
|||
#include "raylib.h" //systemlib |
|||
#include "raygui.h" //included in our source |
|||
#include "nsm.h" //included in our source |
|||
|
|||
//Our own files
|
|||
#include "constants.h" |
|||
#include "programstate.h" |
|||
#include "draw.h" |
|||
#include "jackclient.h" |
|||
#include "gui.h" |
|||
#include "camera.h" |
|||
|
|||
#define OUR_NAME "The Grand Visualizer Show" |
|||
|
|||
//Internal setup
|
|||
static bool readySwitch = false; //switched on once nsm is ready or we start ourselves
|
|||
extern ProgramState programState; |
|||
|
|||
//NSM setup
|
|||
static const char *nsm_url; |
|||
static nsm_client_t *nsm = 0; |
|||
static int wait_nsm = 1; |
|||
|
|||
void callback_nsm_show( void *userdata ) { |
|||
UnhideWindow(); |
|||
programState.applicationWindowVisible = true; //saved. Restored on program start.
|
|||
nsm_send_is_shown(nsm); |
|||
} |
|||
|
|||
void callback_nsm_hide( void *userdata ) { |
|||
HideWindow(); |
|||
programState.applicationWindowVisible = false; //saved. Restored on program start.
|
|||
nsm_send_is_hidden(nsm); |
|||
} |
|||
|
|||
int callback_nsm_save(char **out_msg,void *userdata) { |
|||
saveStateToFile(); |
|||
return ERR_OK; |
|||
} |
|||
|
|||
bool poll_nsm() { |
|||
if (nsm) |
|||
{ |
|||
nsm_check_nowait(nsm); |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
int callback_nsm_open(const char *save_file_path, const char *display_name, const char *client_id, char **out_msg, void *userdata) { |
|||
wait_nsm = 0; |
|||
|
|||
InitWindow(1920, 1080, client_id); |
|||
initProgramState(true, save_file_path, client_id); //true = nsm mode
|
|||
printf("NSM Save File Path: %s, Client Id: %s\n", save_file_path, client_id); |
|||
|
|||
if (strstr(nsm_get_session_manager_features(nsm), ":optional-gui:" )) { |
|||
nsm_set_show_callback(nsm, callback_nsm_show, 0); |
|||
nsm_set_hide_callback(nsm, callback_nsm_hide, 0); |
|||
} |
|||
|
|||
readySwitch = true; |
|||
return ERR_OK; |
|||
} |
|||
|
|||
|
|||
void doExit() { |
|||
if (!nsm_url) { |
|||
saveStateToFile(); |
|||
} |
|||
closeJackClient(); |
|||
//UnloadRenderTexture(renderTarget); // Unload render texture
|
|||
//UnloadShader(shader);
|
|||
free(programState.clockString); |
|||
free(programState.name); |
|||
if (programState.nsm) |
|||
free(programState.filePath); |
|||
CloseWindow(); // Close window and OpenGL context
|
|||
} |
|||
|
|||
void signalExit(int signal) { |
|||
doExit(); |
|||
} |
|||
|
|||
int main(int argc, char **argv) { |
|||
|
|||
signal(SIGINT, signalExit); |
|||
signal(SIGTERM, signalExit); |
|||
|
|||
////////////////////////////////
|
|||
// Init Window (and RayLib) first because file load already creates textures or Commandline and Internal Program State
|
|||
////////////////////////////////
|
|||
//SetConfigFlags(FLAG_WINDOW_RESIZABLE); //When a tiling window manager releases from tiled to floating the window dimensions will be honored.
|
|||
SetConfigFlags(FLAG_MSAA_4X_HINT); // Enable Multi Sampling Anti Aliasing 4x (if available)
|
|||
//Init Window itself is right before setting the name, see below NSM mode callback and standalone branch
|
|||
|
|||
|
|||
|
|||
////////////////////////////////
|
|||
// Setup NSM or Commandline and Internal Program State
|
|||
////////////////////////////////
|
|||
|
|||
//Debug test filePath = "/home/nils/temp/hallo.json";
|
|||
nsm_url = getenv("NSM_URL"); |
|||
//NSM Mode
|
|||
if (nsm_url) { |
|||
nsm = nsm_new(); |
|||
nsm_set_open_callback(nsm, callback_nsm_open, 0); //sets filePath
|
|||
nsm_set_save_callback(nsm, callback_nsm_save, 0); |
|||
int nsmInitErr = nsm_init(nsm, nsm_url); |
|||
if (nsmInitErr == 0) { |
|||
nsm_send_announce(nsm, OUR_NAME, ":optional-gui:", argv[0]); |
|||
} |
|||
else { |
|||
nsm_free( nsm ); |
|||
nsm = 0; |
|||
printf("Tried to start in NSM, but failed.\n"); |
|||
return 0; //End program here. This was started under NSM so we won't fall back to standalone without telling
|
|||
} |
|||
} |
|||
//Standalone Mode
|
|||
else { |
|||
const char * filePath; |
|||
readySwitch = true; |
|||
switch(argc) { //TODO: simple parser for now. Just one file name or nothing.
|
|||
case 1: |
|||
filePath = ""; |
|||
break; |
|||
case 2: |
|||
filePath = argv[1]; //TODO: Make sure this is a .json file extension?
|
|||
break; |
|||
default: |
|||
printf("Unsupported arguments. %s filename.json\n", argv[0]); |
|||
return 0; |
|||
} |
|||
|
|||
InitWindow(1920, 1080, OUR_NAME); |
|||
initProgramState(false, filePath, OUR_NAME); //false = no nsm
|
|||
} |
|||
|
|||
while (!readySwitch) { |
|||
poll_nsm(); //wait for nsm.
|
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
////////////////////////////////
|
|||
// Setup Audio/Video
|
|||
////////////////////////////////
|
|||
createJackClient(); |
|||
|
|||
Note allGUINotes[VIS_PORTS*128]; |
|||
|
|||
for (int port=0; port<VIS_PORTS; port++) { |
|||
for (int midiPitch=0; midiPitch<128; midiPitch++) { //128 is a fixed midi value, no need to dynamically decide the size
|
|||
allGUINotes[port*128 + midiPitch] = (Note){false, 0, 0, 0, 0, 0, 0.0f}; //bool active, int port; int x; int y; int pitch; int velocity; float countdown;
|
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
if (nsm_url) { |
|||
if (programState.applicationWindowVisible == false) { //From the save file. if no save file it will be "true"
|
|||
//TODO: The window will be shown briefly and then hide again. But we need to call InitWindow before HideWindow
|
|||
HideWindow(); |
|||
nsm_send_is_hidden(nsm); |
|||
} |
|||
else { |
|||
UnhideWindow(); |
|||
nsm_send_is_shown(nsm); |
|||
} |
|||
} |
|||
|
|||
/* This may be redundant on the desktop platform
|
|||
EnableCursor(); |
|||
ShowCursor() |
|||
*/ |
|||
|
|||
GuiSetFont(LoadFont("font.ttf")); |
|||
//GuiSetStyle(int control, int property, int value);
|
|||
|
|||
int guiDark = ColorToInt((Color){ 45, 50, 57, 255 }); |
|||
int guiHighlight = ColorToInt((Color){ 55, 61, 69, 127 }); |
|||
int guiBright = ColorToInt(RAYWHITE); |
|||
|
|||
|
|||
GuiSetStyle(DEFAULT, BACKGROUND_COLOR, guiDark); |
|||
|
|||
GuiSetStyle(DEFAULT, LINE_COLOR, guiBright); |
|||
|
|||
GuiSetStyle(DEFAULT, BASE_COLOR_NORMAL, guiDark); |
|||
GuiSetStyle(DEFAULT, BASE_COLOR_FOCUSED, guiHighlight); |
|||
GuiSetStyle(DEFAULT, BASE_COLOR_PRESSED, guiHighlight); |
|||
GuiSetStyle(DEFAULT, BASE_COLOR_DISABLED, guiDark); |
|||
|
|||
GuiSetStyle(DEFAULT, TEXT_COLOR_NORMAL, guiBright); |
|||
GuiSetStyle(DEFAULT, TEXT_COLOR_FOCUSED, guiBright); |
|||
GuiSetStyle(DEFAULT, TEXT_COLOR_PRESSED, guiBright); |
|||
GuiSetStyle(DEFAULT, TEXT_COLOR_DISABLED, guiBright); |
|||
GuiSetStyle(DEFAULT, TEXT_SIZE, 16); //Default = 10
|
|||
GuiSetStyle(DEFAULT, TEXT_SPACING, 1); // DEFAULT = 1
|
|||
|
|||
GuiSetStyle(DEFAULT, BORDER_COLOR_NORMAL, guiBright); |
|||
GuiSetStyle(DEFAULT, BORDER_COLOR_FOCUSED, guiBright); |
|||
GuiSetStyle(DEFAULT, BORDER_COLOR_PRESSED, guiBright); |
|||
GuiSetStyle(DEFAULT, BORDER_COLOR_DISABLED, guiBright); |
|||
|
|||
init_draw(); |
|||
init_gui(); //gui.c
|
|||
|
|||
SetTargetFPS(60); // Frames per second limiter, so we don't run with 100% cpu
|
|||
SetExitKey(0); // "Deactivate" ESC to exit
|
|||
|
|||
|
|||
////////////////////////////////
|
|||
// Main Loop
|
|||
////////////////////////////////
|
|||
while(!WindowShouldClose() && !programState.instructedToQuit ) { // Close Key(deactivated!) or WM Close or internal Quit signal via button or NSM
|
|||
poll_nsm(); //just returns false if not under nsm
|
|||
mainLoop_collectTimingAndTransportInProgramState(); |
|||
mainLoop_cameraReposition(); |
|||
mainLoop_processMidiMessages(allGUINotes); //jackclient.c
|
|||
|
|||
BeginDrawing(); //Z-Order is order of calling
|
|||
//ClearBackground(programState.colors[background]); //Real background
|
|||
ClearBackground(programState.colors[background]); //Real background
|
|||
mainLoop_draw(allGUINotes); //resets programState.guiRedrawNeeded to false each call
|
|||
mainLoop_gui(); //Highest z-order for GUI. Also controls and shortcuts
|
|||
EndDrawing(); |
|||
|
|||
} |
|||
////////////////////////////////
|
|||
// Main Loop End
|
|||
////////////////////////////////
|
|||
|
|||
doExit(); |
|||
} |
|||
|
|||
//TODO und Plan
|
|||
/*
|
|||
* Starfield Farben und Größe als options |
|||
* Die Farbpalette ist momentan generell für dunkle Farben ausgelegt. Starfield z.B. geht davon aus. Wenn nicht dann müsste Starfield custom GUI controls anbieten für seine eigenen 3 Farben. |
|||
|
|||
BPM faktor berechnung muss in eigene Funktion. Den brauch ich überall. |
|||
Das mit dem Farbton muss ich nochmal nachschlagen. Tint ist das. Ich will nen Slider, der das ganze einfärbt, aber nicht die originalfarben komplett ersetzt. Da muss ich wahrscheinlich irgendwas multiplizieren. |
|||
Vielleicht muss ich aus RayGUI den HUE Slider aus dem Colorpicker rausnehmen und um 90° drehen. Wenn der auf "weiß" ist dann ist normal |
|||
|
|||
* Distribution: Raylib statisch eingelinkt? Bzw. einfach die Files reinkopiert. Umbenennen für Debian? |
|||
* Pixeleffekte: Es gibt zwei Layer: Vor den Noten und Hinter den Noten. Da können die effekte ausgewählt werden. Und zwar mit ja/nein. Das ist einfach im backend muss aber in der GUI noch irgendwie gemacht werden. Schnee, Regen, Feuer, Plasma, Blätter fallen, Sternenflug nach vorne. |
|||
Die brauchen auch Opacity pro Layer. |
|||
Pixeleffekte brauch auch noch einen Speed-Multipler, float. |
|||
|
|||
Es gibt für jeden Layer eine Hauptdatei, außer die midivis, die werden direkt angezeigt. |
|||
|
|||
* Bugs: nach fenster resize ist camera und rotation so ein bischen schwupps.. da muss man dann manuell nachbessern. |
|||
* Man muss um eine neue Random Palette zu kriegen erst von der wegwechseln und dann wieder neu dahin. |
|||
* |
|||
* Was jetzt? Zeitachsen, e.g. Piano Roll dynamisch erstellen? Oder Bilder? |
|||
* |
|||
* Zeitachse zurückspulen ist ein Problem. Es sei denn es gibt eine Löschmöglichkeit, die man nicht sieht wenn alles exakt gleich wäre. Oder Kahlschlag: Wenn Transport-Pause ist, dann ist leer. Das ist natürlich hart für analytisches angucken. Aber so ist das ja auch nicht gedacht. |
|||
* Es könnte beides geben: Hold und Clear. Mit Hold gibt es die Möglichkeit das Stück zweimal durchlaufen zu lassen. Dann wären beim zweiten Durchgang schon alle Noten da und man würde nur evtl. Grafikeffekte live sehen, etwa partikeleffekte oder die sprites. |
|||
* |
|||
* Noten ignorieren den Alphawert momentan weil sie velocity als alpha haben. Das könnte eine config werden in Zukunft. Notes Alpha: Velocity, Fixed. |
|||
|
|||
|
|||
|
|||
Pitch Marker per Oktave Option. Und per CC setzen. Dann kann man Modulationen zeigen. Das ist der einstiegspunkt für CC controls. |
|||
|
|||
Quadrant links unten ist für custom GUIs. baut von unten nach oben. |
|||
|
|||
Spiegeleffekte sind immer schön. Meterbridge und unten drunter Spiegel mit Wasser-Shader? |
|||
http://www.afqa123.com/2018/10/08/2d-sprite-reflections/
|
|||
|
|||
*Selbst mit Sprites und Animationsmöglichkeiten per CC ist es immer noch schön simple Modi wie "SNES Tanz" zu haben, die nur auf normale Musik reagiert. das mit den Sprites erfordert ja eigene Kompositionsarbeit |
|||
* |
|||
* Wie verträgt sich die manuelle Kamera mit auto-scrolling "follow midi" modi, die ich noch nicht habe. Die brauchen übrigens einen Timstamp in der Note |
|||
|
|||
* Target FPS einstellen per commandline? Globale Config? Menü? Ist das umstellbar? Kann ich das abrufen ob das überhaupt unterstützt wird? |
|||
* |
|||
* Kamera Mirror? So das selbst die schrift gespiegelt ist? Mit zoom-1 ging es nicht, oder? Ist das vielleicht die Größe auf -x zu setzen? |
|||
* |
|||
* Wie bekomme ich die ttf ins fertige Programm? Zur Not natürlich install. Aber reinkompilieren fände ich besser. Das wird aber noch mehr. Es gibt auch noch verschiedene Bilder die dazu kommen. |
|||
* |
|||
* Die ganzen printf log nachrichten mit meinem Client Namen versehen. und zwar programState.name |
|||
* |
|||
* Raylib kann nicht close->hide machen, so wie Qt. Ich hab die funktionalität erstmal rausgenommen. An WindowShouldClose() hängt noch ne ganze menge mehr dran, die ganze KeyPress logic. Das kann ich nicht umschreiben oder hijacken. Ergo ist mein Programm schließbar. Na dann soll es zumindest den letzten Stand speichern. Vielleicht kann ich glfw direkt benutzen? |
|||
#include <GLFW/glfw3.h> |
|||
* |
|||
* Richtiger commandline parser mit usage und --help und --version |
|||
* |
|||
* Weil rayaudio mit drin ist brauch ich momentan celt und opus als libs. Das rauskriegen. Das liegt aber daran wie raylib kompiliert wurde. Also wenn ich statisch linke... |
|||
|
|||
* Es gibt bestimmt so open game art leute oder demo-scene leute, die nen Music Vizualizer mit diskreten informationen (midi) machen wollen |
|||
* Modus oder parameter per CC verändern. Ganzer Modus ist vielleicht nicht so appealing, aber die Leuchtintensität wäre das ganz cool. z.B. auf CC7 Volume. Oder Wobbly Notes auf Modwheel. Das passt auch zur Lightshow |
|||
* Man muss nicht zwingend reale Musik hier reinschicken. Man kann auch ne Light-Show machen. |
|||
* Jeder Modus braucht aber einen Docstring, in dem stehen muss wie es gedacht ist. Ob das jetzt 16 channel music trigger ist oder per channel light/dance show etc. |
|||
* Jeder Modus kann ja einfach so, ohne Skriptsprache oder config datei, eigene GUI Felder machen. Oberer Bildschirmrand ist für die, untere Hälfte ist generisch. Muss man ja nicht übertreiben. |
|||
|
|||
* Zeit ist das lineare Element, nicht rhythmus. |
|||
* Wie kriegt man Lookahead hin? Für live input geht das also nicht. |
|||
* Man muss das stück zweimal laufen lassen und beim zweiten mal wird dann |
|||
* gerendert. Oder es wird eine statische save-datei erzeugt mit dem zeitindex |
|||
* und beim render-durchlauf wird nur mit jack transport gestartet und gestoppt. |
|||
* |
|||
* CC Controls sind schon cooler als GUI controls. Dann kann man da sachen stufenlos einstellen mit hardware controllern. Dann kann man auch so One-Shot effekte Feuern, wie ne richtige Lightshow. "Add Random Firework Shot" |
|||
* MIDI Learn? So Effekt-Trigger erfordern dann nen Layermodus. Die sind einfach oben drauf. |
|||
* |
|||
* Latency look ahead 30 sekunden, eine Minute. Für Videos, wenn nix live gespielt wird. Dann kann man sehen was kommt und der playback cursor ist in der mitte. Wir sind das tool, dass die latency reported. |
|||
* Gibt es tools, die meinen Code und das Programm auf RT tauglichkeit testen können? |
|||
* https://github.com/fundamental/stoat
|
|||
|
|||
* Text-Einblendungen ("So groß wie möglich" und "Feste Größe mit Wordwrap") über JACK Meta Data, midi karaoke?, spezialport? oder sonst wie? Dann können Programme ihre Titel zeigen. |
|||
* Laufschrift unten drunter im DEMO-Scene style |
|||
* |
|||
* Es kann auch einen modus geben mit Spezial-Animationen von Sprites oder 3D Models, der muss dann aber extra per MIDI Spur programmiert werden mit speziellen werten nur für unser Programm. |
|||
* SNES Sprites mit Tanzschritten oder so. |
|||
* Oder der Winamp 3D Bär |
|||
* |
|||
* In-Scale Mode. Man sucht sich eine Tonart aus und das wird darauf gemappt. |
|||
* Die Notenbreite kann ruhig mehr ausnutzen. Muss natürlich auf die Fenstergröße skaliert sein. Das passt auch dazu wenn bestimmte Tracks nicht verbunden sind. |
|||
* Nicht verbundene Tracks könnte auch von BG-ALTERNATIVE auf BG geschaltet werden. Not Connected: Don't treat differently (default), BG ("deactivated"), Omit |
|||
* |
|||
* Modus Slideshow: Zeige Bilder in einer Reihenfolge. Da wir ja in NSM sind kann man das einfach reinkopieren. der bleibt beim letzten bild stehen oder wrap around. Jede Note ist ein Impuls. Modus 2: 127 Bilder, jeder Pitch ist 000.png bis 127.png |
|||
* |
|||
* CC Switch für Modus Switch (direktanwahl) wäre schon cool. |
|||
* Kann ich parallel malen? Technisch schon, aber nicht mit den noten. die sind momentan an einer position. Ich bräuchte sonst Rects für jede Variante. Und die müssten auch alle immer mit aktiv und countdown parallel verändert werden. Das klingt aber machbar. |
|||
* Das sind meine Layer. Dann brauch ich nur noch nen Z Order die vom user einstellbar ist. |
|||
* JACK schreibt trotzdem immer in den gleichen Array. Der ist dann getrennt von den x,y position. Evtl. önnte ich es auch so machen, dass statt ALLE NOTEN IMMER. Immer nur die in der Datenstruktur sind, die gerade aktiv sind. Das ist vielleicht mit dem zugriff schwieriger? |
|||
* |
|||
* Widget mit Repositionierbaren Listenposition, die an und aus sind. |
|||
* |
|||
* Grafiken, .png und .apng müssen transparenz unterstützen. Dann kann man daraus sowas wie titel und "DANCE!" machen, die mit dem Layer/Overlay Modus oben drüben liegen. |
|||
* |
|||
* Vollbildauflösung muss einstellbar sein. Wird im Save File gespeichert. |
|||
* |
|||
* Bilder und Grafiken von Instrumenten mit Griffzeichnungen |
|||
* Akkord-Analyse für Gitarren. |
|||
* |
|||
* RayLib bug. Wenn wir hidden sind und closeWindow machen kommt: WARNING: GLFW: Error: 65537 Description: The GLFW library is not initialized |
|||
* Leider bringt es auch nix vorher das window auf show zu machen. |
|||
* |
|||
* Readme: |
|||
* Name: The Grand Visualizer Show (TGVS) |
|||
* Englischer Name, weil das kein Teil der LSS ist. |
|||
* Es ist für die eigenen Songs gedacht, am Ende der Production-Phase. |
|||
* Man hat Midi Ausgänge und einzelspuren oder kann zur Not einzelne Spuren mit Aubio in Midi-Signale umwandeln. |
|||
* |
|||
* Ein Instrument for Jack Midi Port. Wenn das ein midi-channel file ist dann muss man vorher splitten. |
|||
* Control-Ports braucht es nicht. Dafür gibt es die nicht belegten CCs? Oder ich suche mir davon welche aus, dann muss ich nicht alle 128 abfragen für alle Ports? |
|||
* Am Ende des Tages gibt es kein universelles Belegungsschema für die Draw Modes. Die müssen halt gelernt werden. |
|||
|
|||
* If further developed at all, this application is bound to run into feature-heaven/hell. |
|||
* There are so many possible ideas and variations how to visualize, and we want them all! |
|||
* With progressing development you can expect a nightmare amount of GUI controls, different rules |
|||
* and behaviour for different program modes and one can only hope that at least the basic drawing |
|||
* modes are straightforward without the need for a video tutorial to just see something on the screen. |
|||
* Be warned: Persons who use the words "KISS" or "UNIX PHILOSOPHY" will be sighed at! :) |
|||
|
|||
winkel für rotation: 360* / 128 = 2.8125° pro midi Step . 64 = 180° . 127 = 357,1875° |
|||
|
|||
* Ist der Sprite Overlay was anderes als die Slideshow? Ja, slideshow ist noten-ports, weil das sehr viel einfacher ist für den benutzer |
|||
|
|||
Sprites und Kontrollport |
|||
* |
|||
* Die Kamera wird über einen speziellen Midi-Kontrollport gesteuert. Alle Parameter absolut (0% links, 64 = Mitte, 127 = 100% rechts des Bildschirm) und Relativ (CC unter 64 = gehe nach links, CC=64 nix, CC über 64=gehe nach rechts) |
|||
* |
|||
* |
|||
*Es gibt Sprites. Das sind bilder vom Hintergrundbild bis kleine Elemente. Man kann die mit der Maus steuern, drag and drop zum platzieren. Die anderen controls muss man sich dann überlegen. |
|||
* Default sind die immer bei 0,0. Die können skaliert werden, mit aspect ratio, auf volle breite. Dann macht natürlich zoom nix mehr. Aber das ist der einfachste Fall. einfach 1920x1080 bilder reinladen und gut ist. |
|||
* |
|||
* Es gibt 128 Sprites. Einen Pro Pitch. Die sind im Control Port. |
|||
* Der momentan zu kontrollierende Sprite wird per Midi-Note ausgewählt. Alle Sprites die momentan Note On sind hören! :) Ne halt, das ist schwierig mit nem Hardware controller? ne, eigentlich geht das.. |
|||
* Die Kamera ist fest auf CC0-4 und der ist es egal ob note on oder nicht. |
|||
|
|||
* Sprites sind in der Kamera mit drin? |
|||
* Sprites haben tilesize und tileindex. Per default ist das auf 100% und 0 |
|||
* |
|||
* |
|||
* CCs sind auf Sprites aufgeteilt. Es gibt 12 Stück mit je 10 CCs. |
|||
* Wird einfach durchnummeriert. Wenn das irgendwann nicht reicht mach ich einfach nen 2. Sprite Port auf. |
|||
* 8 CCs am Ende sind frei. |
|||
* Sprite Z Order ist implizit. Höchster Sprite ist ganz oben. Wird aber in foreground und background getrennt. |
|||
* Sprite CC Parameter: |
|||
* |
|||
* x abs in % |
|||
* y abs in % |
|||
* x delta relative, 64=0 , nach links ist minus pixel, nacht rechts ist plus pixel |
|||
* y delta |
|||
* zoom. 64 = Default |
|||
* Opacity |
|||
* Rotation. 0 = 0° default, ab dann wird nach rechts gedreht und 127 ist dann knapp vor 360° |
|||
* tileindex . default 0 |
|||
* Color/Hue. Irgendwie einen Regler zum einfärben |
|||
* RESERVED FOR FUTURE USE |
|||
|
|||
|
|||
Bilder-Slideshow ist die alphabetische Liste in einem DIR. Ein CC o.ä. switched zum nächsten Bild. Es gibt eine Überblendungseinstellung, wie lange (und mit welchem Effekt?) gecrossfaded wird. Das kann natürlich nur NACH dem Signal kommmen, weil wir keine künstliche Latenz |
|||
haben. Da die Slideshow aber sowieso bewusst midi-programmiert werden muss vom user kann das auch einfach vor der Zeit eingestellt werden, immer auf die vier, und unser effekt braucht dann halt einen Schlag bei 120 bpm. |
|||
*/ |
@ -0,0 +1,689 @@ |
|||
|
|||
/*************************************************************************/ |
|||
/* Copyright (C) 2012 Jonathan Moore Liles */ |
|||
/* Copyright (C) 2020- Nils Hilbricht */ |
|||
/* */ |
|||
/* Permission to use, copy, modify, and/or distribute this software for */ |
|||
/* any purpose with or without fee is hereby granted, provided that the */ |
|||
/* above copyright notice and this permission notice appear in all */ |
|||
/* copies. */ |
|||
/* */ |
|||
/* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL */ |
|||
/* WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED */ |
|||
/* WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE */ |
|||
/* AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL */ |
|||
/* DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR */ |
|||
/* PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER */ |
|||
/* TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR */ |
|||
/* PERFORMANCE OF THIS SOFTWARE. */ |
|||
/*************************************************************************/ |
|||
|
|||
|
|||
/*************************************************************************/ |
|||
/* A simple, callback based C API for NSM clients. */ |
|||
/* */ |
|||
/* Simplified Example: */ |
|||
/* */ |
|||
/* #include "nsm.h" */ |
|||
/* */ |
|||
/* static nsm_client_t *nsm = 0; */ |
|||
/* static int wait_nsm = 1; */ |
|||
/* */ |
|||
/* int */ |
|||
/* cb_nsm_open ( const char *save_file_path, //See API Docs 2.2.2 */ |
|||
/* const char *display_name, //Not useful */ |
|||
/* const char *client_id, //Use as JACK Client Name */ |
|||
/* char **out_msg, */ |
|||
/* void *userdata ) */ |
|||
/* { */ |
|||
/* do_open_stuff(); //Your own function */ |
|||
/* wait_nsm = 0; */ |
|||
/* return ERR_OK; */ |
|||
/* } */ |
|||
/* */ |
|||
/* int */ |
|||
/* cb_nsm_save ( char **out_msg, */ |
|||
/* void *userdata ) */ |
|||
/* { */ |
|||
/* do_save_stuff(); //Your own function */ |
|||
/* return ERR_OK; */ |
|||
/* } */ |
|||
/* */ |
|||
/* void */ |
|||
/* cb_nsm_show ( void *userdata ) */ |
|||
/* { */ |
|||
/* do_show_ui(); //Your own function */ |
|||
/* nsm_send_is_shown ( nsm ); */ |
|||
/* } */ |
|||
/* */ |
|||
/* void */ |
|||
/* cb_nsm_hide ( void *userdata ) */ |
|||
/* { */ |
|||
/* do_hide_ui(); //Your own function */ |
|||
/* nsm_send_is_hidden ( nsm ); */ |
|||
/* } */ |
|||
/* */ |
|||
/* gboolean */ |
|||
/* poll_nsm() */ |
|||
/* { */ |
|||
/* if ( nsm ) */ |
|||
/* { */ |
|||
/* nsm_check_nowait( nsm ); */ |
|||
/* return true; */ |
|||
/* } */ |
|||
/* return false; */ |
|||
/* } */ |
|||
/* */ |
|||
/* int main( int argc, char **argv ) */ |
|||
/* { */ |
|||
/* const char *nsm_url = getenv( "NSM_URL" ); */ |
|||
/* */ |
|||
/* if ( nsm_url ) */ |
|||
/* { */ |
|||
/* nsm = nsm_new(); */ |
|||
/* */ |
|||
/* nsm_set_open_callback( nsm, cb_nsm_open, 0 ); */ |
|||
/* nsm_set_save_callback( nsm, cb_nsm_save, 0 ); */ |
|||
/* */ |
|||
/* if ( 0 == nsm_init( nsm, nsm_url ) ) */ |
|||
/* { */ |
|||
/* nsm_send_announce( nsm, "FOO", ":optional-gui:", argv[0] );*/ |
|||
/* */ |
|||
/* ********************************************************************** */ |
|||
/* This will block for at most 100 sec and */ |
|||
/* waiting for the NSM server open callback. */ |
|||
/* DISCLAIMER: YOU MAY NOT NEED TO DO THAT. */ |
|||
/* ********************************************************************** */ |
|||
/* */ |
|||
/* int timeout = 0; */ |
|||
/* while ( wait_nsm ) */ |
|||
/* { */ |
|||
/* nsm_check_wait( nsm, 500 ); */ |
|||
/* timeout += 1; */ |
|||
/* if ( timeout > 200 ) */ |
|||
/* exit ( 1 ); */ |
|||
/* } */ |
|||
/* */ |
|||
/* ********************************************************************** */ |
|||
/* This will check if the server support optional-gui */ |
|||
/* and connect the callbacks when support is found. */ |
|||
/* If you don't use the above blocking block */ |
|||
/* this could be done in cb_nsm_open() as well. */ |
|||
/* DISCLAIMER: YOU MAY NOT NEED TO DO THAT. */ |
|||
/* ********************************************************************** */ |
|||
/* */ |
|||
/* if ( strstr( nsm_get_session_manager_features ( nsm ), */ |
|||
/* ":optional-gui:" ) ) */ |
|||
/* { */ |
|||
/* nsm_set_show_callback( nsm, cb_nsm_show, 0 ); */ |
|||
/* nsm_set_hide_callback( nsm, cb_nsm_hide, 0 ); */ |
|||
/* } */ |
|||
/* */ |
|||
/* ********************************************************************** */ |
|||
/* */ |
|||
/* do_timeout_add( 200, poll_nsm, Null ); //Your own function */ |
|||
/* } */ |
|||
/* else */ |
|||
/* { */ |
|||
/* nsm_free( nsm ); */ |
|||
/* nsm = 0; */ |
|||
/* } */ |
|||
/* } */ |
|||
/* } */ |
|||
/**************************************************************************/ |
|||
|
|||
#ifndef _NSM_H |
|||
#define _NSM_H |
|||
|
|||
#define NSM_API_VERSION_MAJOR 1 |
|||
#define NSM_API_VERSION_MINOR 0 |
|||
|
|||
#include <lo/lo.h> |
|||
#include <string.h> |
|||
#include <sys/types.h> |
|||
#include <unistd.h> |
|||
#include <stdlib.h> |
|||
#include <stdio.h> |
|||
|
|||
typedef void * nsm_client_t; |
|||
typedef int (nsm_open_callback)( const char *name, const char *display_name, const char *client_id, char **out_msg, void *userdata ); |
|||
typedef int (nsm_save_callback)( char **out_msg, void *userdata ); |
|||
typedef void (nsm_show_gui_callback)( void *userdata ); |
|||
typedef void (nsm_hide_gui_callback)( void *userdata ); |
|||
typedef void (nsm_active_callback)( int b, void *userdata ); |
|||
typedef void (nsm_session_is_loaded_callback)( void *userdata ); |
|||
typedef int (nsm_broadcast_callback)( const char *, lo_message m, void *userdata ); |
|||
|
|||
#define _NSM() ((struct _nsm_client_t*)nsm) |
|||
|
|||
#define NSM_EXPORT __attribute__((unused)) static |
|||
|
|||
/* private parts */ |
|||
struct _nsm_client_t |
|||
{ |
|||
const char *nsm_url; |
|||
|
|||
lo_server _server; |
|||
lo_server_thread _st; |
|||
lo_address nsm_addr; |
|||
|
|||
int nsm_is_active; |
|||
char *nsm_client_id; |
|||
char *_session_manager_name; |
|||
char *_session_manager_features; |
|||
|
|||
nsm_open_callback *open; |
|||
void *open_userdata; |
|||
|
|||
nsm_save_callback *save; |
|||
void *save_userdata; |
|||
|
|||
nsm_show_gui_callback *show; |
|||
void *show_userdata; |
|||
|
|||
nsm_hide_gui_callback *hide; |
|||
void *hide_userdata; |
|||
|
|||
nsm_active_callback *active; |
|||
void *active_userdata; |
|||
|
|||
nsm_session_is_loaded_callback *session_is_loaded; |
|||
void *session_is_loaded_userdata; |
|||
|
|||
nsm_broadcast_callback *broadcast; |
|||
void *broadcast_userdata; |
|||
}; |
|||
|
|||
enum |
|||
{ |
|||
ERR_OK = 0, |
|||
ERR_GENERAL = -1, |
|||
ERR_INCOMPATIBLE_API = -2, |
|||
ERR_BLACKLISTED = -3, |
|||
ERR_LAUNCH_FAILED = -4, |
|||
ERR_NO_SUCH_FILE = -5, |
|||
ERR_NO_SESSION_OPEN = -6, |
|||
ERR_UNSAVED_CHANGES = -7, |
|||
ERR_NOT_NOW = -8 |
|||
}; |
|||
|
|||
NSM_EXPORT |
|||
int |
|||
nsm_is_active ( nsm_client_t *nsm ) |
|||
{ |
|||
return _NSM()->nsm_is_active; |
|||
} |
|||
|
|||
NSM_EXPORT |
|||
const char * |
|||
nsm_get_session_manager_name ( nsm_client_t *nsm ) |
|||
{ |
|||
return _NSM()->_session_manager_name; |
|||
} |
|||
|
|||
NSM_EXPORT |
|||
const char * |
|||
nsm_get_session_manager_features ( nsm_client_t *nsm ) |
|||
{ |
|||
return _NSM()->_session_manager_features; |
|||
} |
|||
|
|||
NSM_EXPORT |
|||
nsm_client_t * |
|||
nsm_new ( void ) |
|||
{ |
|||
struct _nsm_client_t *nsm = (struct _nsm_client_t*)malloc( sizeof( struct _nsm_client_t ) ); |
|||
|
|||
nsm->nsm_url = 0; |
|||
|
|||
nsm->nsm_is_active = 0; |
|||
nsm->nsm_client_id = 0; |
|||
|
|||
nsm->_server = 0; |
|||
nsm->_st = 0; |
|||
nsm->nsm_addr = 0; |
|||
nsm->_session_manager_name = 0; |
|||
nsm->_session_manager_features = 0; |
|||
|
|||
nsm->open = 0; |
|||
nsm->save = 0; |
|||
nsm->show = 0; |
|||
nsm->hide = 0; |
|||
nsm->active = 0; |
|||
nsm->session_is_loaded = 0; |
|||
nsm->broadcast = 0; |
|||
|
|||
return (nsm_client_t *)nsm; |
|||
} |
|||
|
|||
/*******************************************/ |
|||
/* CLIENT TO SERVER INFORMATIONAL MESSAGES */ |
|||
/*******************************************/ |
|||
|
|||
NSM_EXPORT |
|||
void |
|||
nsm_send_is_dirty ( nsm_client_t *nsm ) |
|||
{ |
|||
if ( _NSM()->nsm_is_active ) |
|||
lo_send_from( _NSM()->nsm_addr, _NSM()->_server, LO_TT_IMMEDIATE, "/nsm/client/is_dirty", "" ); |
|||
} |
|||
|
|||
NSM_EXPORT |
|||
void |
|||
nsm_send_is_clean ( nsm_client_t *nsm ) |
|||
{ |
|||
if ( _NSM()->nsm_is_active ) |
|||
lo_send_from( _NSM()->nsm_addr, _NSM()->_server, LO_TT_IMMEDIATE, "/nsm/client/is_clean", "" ); |
|||
} |
|||
|
|||
NSM_EXPORT |
|||
void |
|||
nsm_send_is_shown ( nsm_client_t *nsm ) |
|||
{ |
|||
if ( _NSM()->nsm_is_active ) |
|||
lo_send_from( _NSM()->nsm_addr, _NSM()->_server, LO_TT_IMMEDIATE, "/nsm/client/gui_is_shown", "" ); |
|||
} |
|||
|
|||
NSM_EXPORT |
|||
void |
|||
nsm_send_is_hidden ( nsm_client_t *nsm ) |
|||
{ |
|||
if ( _NSM()->nsm_is_active ) |
|||
lo_send_from( _NSM()->nsm_addr, _NSM()->_server, LO_TT_IMMEDIATE, "/nsm/client/gui_is_hidden", "" ); |
|||
} |
|||
|
|||
NSM_EXPORT |
|||
void |
|||
nsm_send_progress ( nsm_client_t *nsm, float p ) |
|||
{ |
|||
if ( _NSM()->nsm_is_active ) |
|||
lo_send_from( _NSM()->nsm_addr, _NSM()->_server, LO_TT_IMMEDIATE, "/nsm/client/progress", "f", p ); |
|||
} |
|||
|
|||
NSM_EXPORT |
|||
void |
|||
nsm_send_message ( nsm_client_t *nsm, int priority, const char *msg ) |
|||
{ |
|||
if ( _NSM()->nsm_is_active ) |
|||
lo_send_from( _NSM()->nsm_addr, _NSM()->_server, LO_TT_IMMEDIATE, "/nsm/client/message", "is", priority, msg ); |
|||
} |
|||
|
|||
NSM_EXPORT void |
|||
nsm_send_announce ( nsm_client_t *nsm, const char *app_name, const char *capabilities, const char *process_name ) |
|||
{ |
|||
lo_address to = lo_address_new_from_url( _NSM()->nsm_url ); |
|||
|
|||
if ( ! to ) |
|||
{ |
|||
fprintf( stderr, "NSM: Bad address!" ); |
|||
return; |
|||
} |
|||
|
|||
int pid = (int)getpid(); |
|||
|
|||
lo_send_from( to, _NSM()->_server, LO_TT_IMMEDIATE, "/nsm/server/announce", "sssiii", |
|||
app_name, |
|||
capabilities, |
|||
process_name, |
|||
NSM_API_VERSION_MAJOR, |
|||
NSM_API_VERSION_MINOR, |
|||
pid ); |
|||
|
|||
lo_address_free( to ); |
|||
} |
|||
|
|||
NSM_EXPORT void |
|||
nsm_send_broadcast ( nsm_client_t *nsm, lo_message msg ) |
|||
{ |
|||
if ( _NSM()->nsm_is_active ) |
|||
lo_send_message_from( _NSM()->nsm_addr, _NSM()->_server, "/nsm/server/broadcast", msg ); |
|||
} |
|||
|
|||
|
|||
|
|||
NSM_EXPORT |
|||
void |
|||
nsm_check_wait ( nsm_client_t *nsm, int timeout ) |
|||
{ |
|||
if ( lo_server_wait( _NSM()->_server, timeout ) ) |
|||
while ( lo_server_recv_noblock( _NSM()->_server, 0 ) ) {} |
|||
} |
|||
|
|||
NSM_EXPORT |
|||
void |
|||
nsm_check_nowait (nsm_client_t *nsm ) |
|||
{ |
|||
nsm_check_wait( nsm, 0 ); |
|||
} |
|||
|
|||
|
|||
NSM_EXPORT |
|||
void |
|||
nsm_thread_start ( nsm_client_t *nsm ) |
|||
{ |
|||
lo_server_thread_start( _NSM()->_st ); |
|||
} |
|||
|
|||
|
|||
NSM_EXPORT |
|||
void |
|||
nsm_thread_stop ( nsm_client_t *nsm ) |
|||
{ |
|||
lo_server_thread_stop( _NSM()->_st ); |
|||
} |
|||
|
|||
|
|||
|
|||
NSM_EXPORT void |
|||
nsm_free ( nsm_client_t *nsm ) |
|||
{ |
|||
if ( _NSM()->_st ) |
|||
nsm_thread_stop( nsm ); |
|||
|
|||
if ( _NSM()->_st ) |
|||
lo_server_thread_free( _NSM()->_st ); |
|||
else |
|||
lo_server_free( _NSM()->_server ); |
|||
|
|||
lo_address_free(_NSM()->nsm_addr); |
|||
free(_NSM()->nsm_client_id); |
|||
free(_NSM()->_session_manager_name); |
|||
free(_NSM()->_session_manager_features); |
|||
|
|||
free( _NSM() ); |
|||
} |
|||
|
|||
/*****************/ |
|||
/* SET CALLBACKS */ |
|||
/*****************/ |
|||
|
|||
NSM_EXPORT |
|||
void |
|||
nsm_set_open_callback( nsm_client_t *nsm, nsm_open_callback *open_callback, void *userdata ) |
|||
{ |
|||
_NSM()->open = open_callback; |
|||
_NSM()->open_userdata = userdata; |
|||
} |
|||
|
|||
NSM_EXPORT |
|||
void |
|||
nsm_set_save_callback( nsm_client_t *nsm, nsm_save_callback *save_callback, void *userdata ) |
|||
{ |
|||
_NSM()->save = save_callback; |
|||
_NSM()->save_userdata = userdata; |
|||
|
|||
} |
|||
|
|||
NSM_EXPORT |
|||
void |
|||
nsm_set_show_callback( nsm_client_t *nsm, nsm_show_gui_callback *show_callback, void *userdata ) |
|||
{ |
|||
_NSM()->show = show_callback; |
|||
_NSM()->show_userdata = userdata; |
|||
} |
|||
|
|||
NSM_EXPORT |
|||
void |
|||
nsm_set_hide_callback( nsm_client_t *nsm, nsm_hide_gui_callback *hide_callback, void *userdata ) |
|||
{ |
|||
_NSM()->hide = hide_callback; |
|||
_NSM()->hide_userdata = userdata; |
|||
} |
|||
|
|||
NSM_EXPORT |
|||
void |
|||
nsm_set_active_callback( nsm_client_t *nsm, nsm_active_callback *active_callback, void *userdata ) |
|||
{ |
|||
_NSM()->active = active_callback; |
|||
_NSM()->active_userdata = userdata; |
|||
} |
|||
|
|||
NSM_EXPORT |
|||
void |
|||
nsm_set_session_is_loaded_callback( nsm_client_t *nsm, nsm_session_is_loaded_callback *session_is_loaded_callback, void *userdata ) |
|||
{ |
|||
_NSM()->session_is_loaded = session_is_loaded_callback; |
|||
_NSM()->session_is_loaded_userdata = userdata; |
|||
} |
|||
|
|||
|
|||
NSM_EXPORT |
|||
void |
|||
nsm_set_broadcast_callback( nsm_client_t *nsm, nsm_broadcast_callback *broadcast_callback, void *userdata ) |
|||
{ |
|||
_NSM()->broadcast = broadcast_callback; |
|||
_NSM()->broadcast_userdata = userdata; |
|||
} |
|||
|
|||
|
|||
|
|||
/****************/ |
|||
/* OSC HANDLERS */ |
|||
/****************/ |
|||
|
|||
#undef OSC_REPLY |
|||
#undef OSC_REPLY_ERR |
|||
|
|||
#define OSC_REPLY( value ) lo_send_from( ((struct _nsm_client_t*)user_data)->nsm_addr, ((struct _nsm_client_t*)user_data)->_server, LO_TT_IMMEDIATE, "/reply", "ss", path, value ) |
|||
|
|||
#define OSC_REPLY_ERR( errcode, value ) lo_send_from( ((struct _nsm_client_t*)user_data)->nsm_addr, ((struct _nsm_client_t*)user_data)->_server, LO_TT_IMMEDIATE, "/error", "sis", path, errcode, value ) |
|||
|
|||
|
|||
NSM_EXPORT int _nsm_osc_open ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) |
|||
{ |
|||
(void) types; |
|||
(void) argc; |
|||
(void) msg; |
|||
|
|||
char *out_msg = NULL; |
|||
|
|||
struct _nsm_client_t *nsm = (struct _nsm_client_t*)user_data; |
|||
|
|||
nsm->nsm_client_id = strdup( &argv[2]->s ); |
|||
|
|||
if ( ! nsm->open ) |
|||
return 0; |
|||
|
|||
int r = nsm->open( &argv[0]->s, &argv[1]->s, &argv[2]->s, &out_msg, nsm->open_userdata ); |
|||
|
|||
if ( r ) |
|||
OSC_REPLY_ERR( r, ( out_msg ? out_msg : "") ); |
|||
else |
|||
OSC_REPLY( "OK" ); |
|||
|
|||
if ( out_msg ) |
|||
free( out_msg ); |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
NSM_EXPORT int _nsm_osc_save ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) |
|||
{ |
|||
(void) types; |
|||
(void) argv; |
|||
(void) argc; |
|||
(void) msg; |
|||
|
|||
char *out_msg = NULL; |
|||
|
|||
struct _nsm_client_t *nsm = (struct _nsm_client_t*)user_data; |
|||
|
|||
if ( ! nsm->save ) |
|||
return 0; |
|||
|
|||
int r = nsm->save(&out_msg, nsm->save_userdata ); |
|||
|
|||
if ( r ) |
|||
OSC_REPLY_ERR( r, ( out_msg ? out_msg : "") ); |
|||
else |
|||
OSC_REPLY( "OK" ); |
|||
|
|||
if ( out_msg ) |
|||
free( out_msg ); |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
NSM_EXPORT int _nsm_osc_announce_reply ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) |
|||
{ |
|||
(void) path; |
|||
(void) types; |
|||
(void) argc; |
|||
|
|||
if ( strcmp( &argv[0]->s, "/nsm/server/announce" ) ) |
|||
return -1; |
|||
|
|||
struct _nsm_client_t *nsm = (struct _nsm_client_t*)user_data; |
|||
|
|||
fprintf( stderr, "NSM: Successfully registered. NSM says: %s", &argv[1]->s ); |
|||
|
|||
nsm->nsm_is_active = 1; |
|||
nsm->_session_manager_name = strdup( &argv[2]->s ); |
|||
nsm->_session_manager_features = strdup( &argv[3]->s ); |
|||
nsm->nsm_addr = lo_address_new_from_url( lo_address_get_url( lo_message_get_source( msg ) )); |
|||
|
|||
if ( nsm->active ) |
|||
nsm->active( nsm->nsm_is_active, nsm->active_userdata ); |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
NSM_EXPORT int _nsm_osc_error ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) |
|||
{ |
|||
(void) path; |
|||
(void) types; |
|||
(void) argc; |
|||
(void) msg; |
|||
|
|||
if ( strcmp( &argv[0]->s, "/nsm/server/announce" ) ) |
|||
return -1; |
|||
|
|||
struct _nsm_client_t *nsm = (struct _nsm_client_t*)user_data; |
|||
|
|||
fprintf( stderr, "NSM: Failed to register with NSM server: %s", &argv[2]->s ); |
|||
|
|||
nsm->nsm_is_active = 0; |
|||
|
|||
if ( nsm->active ) |
|||
nsm->active( nsm->nsm_is_active, nsm->active_userdata ); |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
NSM_EXPORT int _nsm_osc_session_is_loaded ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) |
|||
{ |
|||
(void) path; |
|||
(void) types; |
|||
(void) argv; |
|||
(void) argc; |
|||
(void) msg; |
|||
|
|||
struct _nsm_client_t *nsm = (struct _nsm_client_t*)user_data; |
|||
|
|||
if ( ! nsm->session_is_loaded ) |
|||
return 0; |
|||
|
|||
nsm->session_is_loaded( nsm->session_is_loaded_userdata ); |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
NSM_EXPORT int _nsm_osc_show ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) |
|||
{ |
|||
|
|||
struct _nsm_client_t *nsm = (struct _nsm_client_t*)user_data; |
|||
|
|||
if ( ! nsm->show ) |
|||
return 0; |
|||
|
|||
nsm->show( nsm->show_userdata ); |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
NSM_EXPORT int _nsm_osc_hide ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) |
|||
{ |
|||
|
|||
struct _nsm_client_t *nsm = (struct _nsm_client_t*)user_data; |
|||
|
|||
if ( ! nsm->hide ) |
|||
return 0; |
|||
|
|||
nsm->hide( nsm->hide_userdata ); |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
NSM_EXPORT int _nsm_osc_broadcast ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) |
|||
{ |
|||
(void) types; |
|||
(void) argv; |
|||
(void) argc; |
|||
|
|||
struct _nsm_client_t *nsm = (struct _nsm_client_t*)user_data; |
|||
|
|||
if ( ! nsm->broadcast ) |
|||
return 0; |
|||
|
|||
return nsm->broadcast( path, msg, nsm->broadcast_userdata ); |
|||
} |
|||
|
|||
|
|||
|
|||
NSM_EXPORT |
|||
int |
|||
nsm_init ( nsm_client_t *nsm, const char *nsm_url ) |
|||
{ |
|||
_NSM()->nsm_url = nsm_url; |
|||
|
|||
lo_address addr = lo_address_new_from_url( nsm_url ); |
|||
int proto = lo_address_get_protocol( addr ); |
|||
lo_address_free( addr ); |
|||
|
|||
_NSM()->_server = lo_server_new_with_proto( NULL, proto, NULL ); |
|||
|
|||
if ( ! _NSM()->_server ) |
|||
return -1; |
|||
|
|||
lo_server_add_method( _NSM()->_server, "/error", "sis", _nsm_osc_error, _NSM() ); |
|||
lo_server_add_method( _NSM()->_server, "/reply", "ssss", _nsm_osc_announce_reply, _NSM() ); |
|||
lo_server_add_method( _NSM()->_server, "/nsm/client/open", "sss", _nsm_osc_open, _NSM() ); |
|||
lo_server_add_method( _NSM()->_server, "/nsm/client/save", "", _nsm_osc_save, _NSM() ); |
|||
lo_server_add_method( _NSM()->_server, "/nsm/client/session_is_loaded", "", _nsm_osc_session_is_loaded, _NSM() ); |
|||
lo_server_add_method( _NSM()->_server, "/nsm/client/show_optional_gui", "", _nsm_osc_show, _NSM() ); |
|||
lo_server_add_method( _NSM()->_server, "/nsm/client/hide_optional_gui", "", _nsm_osc_hide, _NSM() ); |
|||
lo_server_add_method( _NSM()->_server, NULL, NULL, _nsm_osc_broadcast, _NSM() ); |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
|
|||
NSM_EXPORT |
|||
int |
|||
nsm_init_thread ( nsm_client_t *nsm, const char *nsm_url ) |
|||
{ |
|||
_NSM()->nsm_url = nsm_url; |
|||
|
|||
lo_address addr = lo_address_new_from_url( nsm_url ); |
|||
int proto = lo_address_get_protocol( addr ); |
|||
lo_address_free( addr ); |
|||
|
|||
_NSM()->_st = lo_server_thread_new_with_proto( NULL, proto, NULL ); |
|||
_NSM()->_server = lo_server_thread_get_server( _NSM()->_st ); |
|||
|
|||
if ( ! _NSM()->_server ) |
|||
return -1; |
|||
|
|||
lo_server_thread_add_method( _NSM()->_st, "/error", "sis", _nsm_osc_error, _NSM() ); |
|||
lo_server_thread_add_method( _NSM()->_st, "/reply", "ssss", _nsm_osc_announce_reply, _NSM() ); |
|||
lo_server_thread_add_method( _NSM()->_st, "/nsm/client/open", "sss", _nsm_osc_open, _NSM() ); |
|||
lo_server_thread_add_method( _NSM()->_st, "/nsm/client/save", "", _nsm_osc_save, _NSM() ); |
|||
lo_server_thread_add_method( _NSM()->_st, "/nsm/client/session_is_loaded", "", _nsm_osc_session_is_loaded, _NSM() ); |
|||
lo_server_thread_add_method( _NSM()->_st, "/nsm/client/show_optional_gui", "", _nsm_osc_show, _NSM() ); |
|||
lo_server_thread_add_method( _NSM()->_st, "/nsm/client/hide_optional_gui", "", _nsm_osc_hide, _NSM() ); |
|||
lo_server_thread_add_method( _NSM()->_st, NULL, NULL, _nsm_osc_broadcast, _NSM() ); |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
#endif /* NSM_H */ |
@ -0,0 +1,248 @@ |
|||
//Standard lib
|
|||
#include <stdio.h> |
|||
#include <stdlib.h> |
|||
#include <string.h> |
|||
#include <assert.h> |
|||
#include <time.h> |
|||
#include <sys/stat.h> |
|||
#include <sys/types.h> |
|||
|
|||
//Third party
|
|||
#include "raylib.h" |
|||
#include "jRead.h" //included in our source https://www.codeproject.com/Articles/885389/jRead-An-in-place-JSON-Element-Reader |
|||
#include "jWrite.h" //included in our source https://www.codeproject.com/Articles/887604/jWrite-A-Really-Simple-JSON-Writer-in-C |
|||
|
|||
//Our own files
|
|||
#include "constants.h" |
|||
#include "programstate.h" |
|||
#include "drawhelper.h" |
|||
#include "draw.h" |
|||
|
|||
#define SAVE_BUFFER_LEN 1024*1024*4 //4 MB to be on the safe side. This is just RAM, not the file size.
|
|||
|
|||
ProgramState programState; //global singleton state. All other files use extern ProgramState programState;
|
|||
|
|||
void setColorPalette(int number) { |
|||
|
|||
//First set Background to default, in case a palette only works with notes.
|
|||
//Otherwise the palette can overwrite.
|
|||
|
|||
srand(time(0)); |
|||
|
|||
programState.colors[VIS_PORTS+0] = (Color){ 45, 50, 57, 255 }; //LSS Background color. The general background, lowest layer.
|
|||
programState.colors[VIS_PORTS+1] = (Color){ 55, 61, 69, 127 }; //used for note backgrounds
|
|||
programState.colors[VIS_PORTS+2] = (Color){ 65, 65, 65, 200 }; //e.g. pitch marker
|
|||
|
|||
switch (number) { |
|||
case 1: //random notes. Background default
|
|||
for (int i=0; i<VIS_PORTS; i++) { |
|||
programState.colors[i] = (Color) { GetRandomValue(0,255), GetRandomValue(0,255), GetRandomValue(0,255), 255 }; |
|||
} |
|||
break; |
|||
|
|||
case 2: //greyscale
|
|||
for (int i=0; i<VIS_PORTS; i++) { |
|||
programState.colors[i] = (Color) { (255/VIS_PORTS)*i, (255/VIS_PORTS)*i, (255/VIS_PORTS)*i, 255 }; |
|||
} |
|||
break; |
|||
|
|||
|
|||
default: //incl. 0
|
|||
for (int i=0; i<VIS_PORTS; i++) { |
|||
programState.colors[i] = intToColor(i, VIS_PORTS); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void initProgramState(bool nsm, const char * filePath, const char * programName) { |
|||
//Initial Global State
|
|||
//Some values may be overwritten directly after init by loading from a file, such as drawMode
|
|||
//Others, such as guiRedrawNeeded, are dynamic values.
|
|||
|
|||
//NSM (New Session Manager) is a special state that decides mostly about program paths.
|
|||
//With NSM=true filePath will be a directory and we save under it. Any images loaded are symlinked into that directory
|
|||
//With NSM=false filePath will be a direct (json) file, given by the user. Images will be loaded from their original place.
|
|||
programState.nsm = nsm; |
|||
|
|||
programState.camera = (Camera2D){ 0 }; //Not saved itself, but it values will be.
|
|||
|
|||
//Saved
|
|||
|
|||
programState.showPitchMarker = false; |
|||
programState.pitchMarkerValue = 60; //middle C
|
|||
programState.showPortBackground = true; // Background for notes, not the layer
|
|||
programState.showPortBackgroundForUnconnected = true; // Draw background also for unconnected ports
|
|||
programState.fadeOutMode = 1; // 0 = framelength * programState.fadeOutFactor, 1 = bpm, 2 = instant off
|
|||
programState.fadeOutFactor = 3; //higher=faster. must be >= 1. multiplied with time of one frame (0.016s @ 60fps). Factor 3 is the same as bpm fadeout with 120bpm
|
|||
programState.showConnectedPortnames = true; |
|||
programState.includeClientInPortNameDisplay = false; |
|||
programState.alwaysShowClock = false; |
|||
programState.applicationWindowVisible = true; //for nsm
|
|||
//.camera is not saved, but we will save the values individually
|
|||
programState.camera.zoom = 1.0f; |
|||
programState.camera.rotation = 0.0f; |
|||
programState.cameraCenterOnX = 0; |
|||
programState.cameraCenterOnY = 0; |
|||
programState.pitchMin = 0+2*12; //Note Drawing Range for all modes
|
|||
programState.pitchMax = 127-2*12; //Pitch is inclusive. Note Drawing Range for all modes
|
|||
|
|||
//Layer Switches. Also see enum in constants.h
|
|||
programState.showBackgroundImageLayer = false; //or plain color
|
|||
programState.showEffectLayer = true; |
|||
programState.showSpriteLayer = true; |
|||
programState.showDrawMode = true; |
|||
|
|||
//Layer Modes and Settings
|
|||
//char * backgroundImagePath;
|
|||
programState.drawMode = 0; |
|||
|
|||
setColorPalette(0); |
|||
|
|||
//For specific modes only
|
|||
programState.meterbridge_grouping = 1; // 127 / 1 as default -> show all notes individually. 12 would mean show activity in one octave as the same rectangle. pitchmarker is the root note.
|
|||
|
|||
//Images and paths
|
|||
programState.pathBackgroundImage = ""; //set by load or draw.h. This is a path, not the texture.
|
|||
|
|||
//Not Saved
|
|||
programState.clockString = malloc (sizeof(char) * 255); //set by jack
|
|||
programState.guiRedrawNeeded = true; //screen size or drawing configuration changed
|
|||
programState.instructedToQuit = false; //internal signal for the quit button
|
|||
programState.guiVisible = false; //Press ESC
|
|||
programState.bpm = 0.0d; //set by jack
|
|||
//programState.transportRolling;
|
|||
programState.modeDescription = ""; // \n for line break. Label to describe a mode. Set by files like draw_port_grids.c etc.
|
|||
//programState.connectedPortNames #empty
|
|||
programState.portActivity[VIS_PORTS]; |
|||
programState.lockInput = false; //Prevent keypresses while in file open dialog etc.
|
|||
|
|||
//Not saved but provided or changed by NSM or args
|
|||
programState.name = strdup(programName); |
|||
if (nsm) { |
|||
programState.nsmDirectory = strdup(filePath); //Our directory in NSM, empty standalone. Don't use for checks, use bool programState.nsm instead
|
|||
programState.filePath = malloc (sizeof(char) * 4096); //4096 is the max path length in linux
|
|||
snprintf(programState.filePath, (sizeof(char) * 4096), "%s/%s", filePath, "tgvssave.json"); |
|||
mkdir(programState.nsmDirectory, 0755); //let silently fail if already exists. We need the dir to symlink image files.
|
|||
} |
|||
else { |
|||
programState.filePath = strdup(filePath); //This is always the direct json save file.
|
|||
} |
|||
loadStateFromFile(); |
|||
} |
|||
|
|||
void loadStateFromFile() { |
|||
//call only after initProgramState
|
|||
if (programState.nsm) { |
|||
} |
|||
|
|||
if (programState.filePath && programState.filePath[0] != '\0') { |
|||
char * buffer; |
|||
buffer = LoadFileText(programState.filePath); // Load text data from file (read), returns a '\0' terminated string
|
|||
|
|||
|
|||
//Begin de-serializing save data
|
|||
if (buffer) { |
|||
|
|||
//printf("%s\n", buffer); //print whole json file.
|
|||
|
|||
//A third parameter NULL is for jRead query params and recommended as default
|
|||
programState.showPitchMarker = (bool)jRead_int(buffer, "{'showPitchMarker'", NULL); |
|||
programState.pitchMarkerValue = jRead_int(buffer, "{'pitchMarkerValue'", NULL); |
|||
programState.showPortBackground = (bool)jRead_int(buffer, "{'showPortBackground'", NULL); |
|||
programState.showPortBackgroundForUnconnected = (bool)jRead_int(buffer, "{'showPortBackgroundForUnconnected'", NULL); |
|||
programState.fadeOutMode = jRead_int(buffer, "{'fadeOutMode'", NULL); |
|||
programState.fadeOutFactor = jRead_int(buffer, "{'fadeOutFactor'", NULL); |
|||
programState.showConnectedPortnames = (bool)jRead_int(buffer, "{'showConnectedPortnames'", NULL); |
|||
programState.includeClientInPortNameDisplay = (bool)jRead_int(buffer, "{'includeClientInPortNameDisplay'", NULL); |
|||
programState.alwaysShowClock = (bool)jRead_int(buffer, "{'alwaysShowClock'", NULL); |
|||
programState.applicationWindowVisible = (bool)jRead_int(buffer, "{'applicationWindowVisible'", NULL); |
|||
programState.cameraCenterOnX = jRead_int(buffer, "{'cameraCenterOnX'", NULL); |
|||
programState.cameraCenterOnY = jRead_int(buffer, "{'cameraCenterOnY'", NULL); |
|||
programState.camera.zoom = (float)jRead_double(buffer, "{'camera.zoom'", NULL); |
|||
programState.camera.rotation = (float)jRead_double(buffer, "{'camera.rotation'", NULL); |
|||
programState.pitchMin = jRead_int(buffer, "{'pitchMin'", NULL); |
|||
programState.pitchMax = jRead_int(buffer, "{'pitchMax'", NULL); |
|||
programState.meterbridge_grouping = jRead_int(buffer, "{'meterbridge_grouping'", NULL); |
|||
|
|||
programState.drawMode = jRead_int(buffer, "{'drawMode'", NULL); |
|||
|
|||
programState.showBackgroundImageLayer = (bool)jRead_int(buffer, "{'showBackgroundImageLayer'", NULL); |
|||
programState.showEffectLayer = (bool)jRead_int(buffer, "{'showEffectLayer'", NULL); |
|||
programState.showSpriteLayer = (bool)jRead_int(buffer, "{'showSpriteLayer'", NULL); |
|||
programState.showDrawMode = (bool)jRead_int(buffer, "{'showDrawMode'", NULL); |
|||
|
|||
//int jRead_string( char *pJson, char *pQuery, char *pDest, int destlen, int *queryParams );
|
|||
char bgimgpath[4096]; |
|||
jRead_string(buffer, "{'pathBackgroundImage'", bgimgpath, 4096 , NULL); |
|||
loadBackgroundImage(bgimgpath, false); //sets programState.pathBackgroundImage
|
|||
|
|||
|
|||
char colorKey[64]; |
|||
for (int i=0; i<19; i++) { |
|||
sprintf(colorKey, "{'colors_%i'", i); |
|||
//Thanks jRead! we saved the color value as string, because the long-write function does not exist. But luckily it parses everything back during read :).
|
|||
programState.colors[i] = GetColor(jRead_long(buffer, colorKey, NULL)); |
|||
} |
|||
|
|||
} |
|||
else { |
|||
printf("Unable to open %s. Starting with default program state. Expected for first run.\n", programState.filePath); |
|||
} |
|||
} |
|||
else { |
|||
printf("No filepath present. Not loading anything, every setting is temporary.\n"); |
|||
|
|||
} |
|||
} |
|||
|
|||
void saveStateToFile() { |
|||
//call only after initProgramState
|
|||
|
|||
if (programState.filePath && programState.filePath[0] != '\0') { |
|||
char buffer[SAVE_BUFFER_LEN]; |
|||
int returnJWCode; |
|||
|
|||
jwOpen( buffer, SAVE_BUFFER_LEN, JW_OBJECT, JW_PRETTY ); // open root node as object
|
|||
//jwObj_string( "key", "value" );
|
|||
jwObj_int( "showPitchMarker", (int)programState.showPitchMarker ); |
|||
jwObj_int( "pitchMarkerValue", programState.pitchMarkerValue ); |
|||
jwObj_int( "showPortBackground", (int)programState.showPortBackground ); |
|||
jwObj_int( "showPortBackgroundForUnconnected", (int)programState.showPortBackgroundForUnconnected ); |
|||
jwObj_int( "fadeOutMode", programState.fadeOutMode ); |
|||
jwObj_int( "fadeOutFactor", programState.fadeOutFactor ); |
|||
jwObj_int( "showConnectedPortnames", (int)programState.showConnectedPortnames ); |
|||
jwObj_int( "includeClientInPortNameDisplay", (int)programState.includeClientInPortNameDisplay ); |
|||
jwObj_int( "alwaysShowClock", (int)programState.alwaysShowClock ); |
|||
jwObj_int( "applicationWindowVisible", (int)programState.applicationWindowVisible ); |
|||
jwObj_int( "cameraCenterOnX", programState.cameraCenterOnX ); |
|||
jwObj_int( "cameraCenterOnY", programState.cameraCenterOnY ); |
|||
jwObj_double( "camera.zoom", (double)programState.camera.zoom ); |
|||
jwObj_double( "camera.rotation", (double)programState.camera.rotation ); |
|||
jwObj_int( "pitchMin", programState.pitchMin ); |
|||
jwObj_int( "pitchMax", programState.pitchMax ); |
|||
jwObj_int( "meterbridge_grouping", programState.meterbridge_grouping ); |
|||
|
|||
jwObj_int( "drawMode", programState.drawMode ); |
|||
|
|||
jwObj_int( "showBackgroundImageLayer", (int)programState.showBackgroundImageLayer ); |
|||
jwObj_int( "showEffectLayer", (int)programState.showEffectLayer ); |
|||
jwObj_int( "showSpriteLayer", (int)programState.showSpriteLayer ); |
|||
jwObj_int( "showDrawMode", (int)programState.showDrawMode ); |
|||
|
|||
jwObj_string( "pathBackgroundImage", programState.pathBackgroundImage ); |
|||
|
|||
|
|||
char colorKey[64]; |
|||
char colorValue[256]; |
|||
for (int i=0; i<19; i++) { |
|||
sprintf(colorKey, "colors_%i", i); |
|||
sprintf(colorValue, "%li", ColorToInt(programState.colors[i])); |
|||
jwObj_string( colorKey, colorValue ); |
|||
} |
|||
|
|||
returnJWCode = jwClose(); |
|||
|
|||
SaveFileText(programState.filePath, buffer); // Save text data to file (write), string must be '\0' terminated
|
|||
} |
|||
} |
|||
|
@ -0,0 +1,79 @@ |
|||
#ifndef PROGRAMSTATE_H |
|||
#define PROGRAMSTATE_H |
|||
|
|||
#include "constants.h" |
|||
|
|||
//Singleton. This is also saved and loaded. See main.c
|
|||
typedef struct { |
|||
|
|||
bool nsm; //under nsm or not
|
|||
|
|||
//Saved
|
|||
/////////////////////
|
|||
bool guiVisible; |
|||
bool showPitchMarker; |
|||
int pitchMarkerValue; |
|||
bool showPortBackground; |
|||
bool showPortBackgroundForUnconnected; |
|||
int fadeOutMode; |
|||
int fadeOutFactor; |
|||
bool showConnectedPortnames; |
|||
bool includeClientInPortNameDisplay; |
|||
bool alwaysShowClock; |
|||
bool applicationWindowVisible; |
|||
int cameraCenterOnX; |
|||
int cameraCenterOnY; |
|||
int pitchMin; //Note Drawing Range for all modes
|
|||
int pitchMax; //Note Drawing Range for all modes
|
|||
|
|||
//Layer Switches
|
|||
|
|||
bool showBackgroundImageLayer; //or plain color
|
|||
bool showEffectLayer; |
|||
bool showSpriteLayer; |
|||
bool showDrawMode; |
|||
bool showForegroundSpritesLayer; |
|||
bool showForegroundEffectsLayer; |
|||
|
|||
//Layer Modes and Settings
|
|||
//char * backgroundImagePath;
|
|||
|
|||
int drawMode; //Vertical, Grids...
|
|||
|
|||
Color colors[VIS_PORTS+3]; |
|||
|
|||
//For specific modes only
|
|||
int meterbridge_grouping; // 127 / 1 as default -> show all notes individually. 12 would mean show activity in one octave as the same rectangle. pitchmarker is the root note.
|
|||
|
|||
//Images and paths
|
|||
char * pathBackgroundImage; //in nsm this is always background.png , standalone it is an absolute file path
|
|||
|
|||
//Not Saved
|
|||
/////////////////////
|
|||
bool guiRedrawNeeded; |
|||
bool instructedToQuit; |
|||
bool transportRolling; |
|||
double bpm; |
|||
char* clockString; |
|||
const char* connectedPortNames[VIS_PORTS]; |
|||
const char * modeDescription; //48 chars per line.\n for line break. Label to describe a mode. Set by files like draw_port_grids.c etc.
|
|||
int portActivity[VIS_PORTS]; //0 means no activity, any other number is the number of current note-ons
|
|||
bool lockInput; //Prevent keypresses while in file open dialog etc.
|
|||
Camera2D camera; |
|||
|
|||
//Not saved but provided by NSM or argc
|
|||
/////////////////////
|
|||
char * name; |
|||
char * nsmDirectory; //Our directory in NSM, empty standalone. Don't use for checks, use bool programState.nsm instead
|
|||
char * filePath; //This is always the direct json save file. There is only one filePath per session. We do not provide "reload" or "open"
|
|||
|
|||
} ProgramState; |
|||
|
|||
|
|||
void initProgramState(bool nsm, const char * filePath, const char * programName); |
|||
void loadStateFromFile(); |
|||
void saveStateToFile(); |
|||
void setColorPalette(int number); |
|||
|
|||
|
|||
#endif // not defined PROGRAMSTATE_H
|
File diff suppressed because it is too large
Loading…
Reference in new issue