wip: refactor into api and app

This commit is contained in:
🪞👃🪞 2024-11-09 19:50:22 +01:00
parent 051c3d3663
commit 70fc3c97d1
61 changed files with 453 additions and 255 deletions

22
Cargo.lock generated
View file

@ -2657,6 +2657,28 @@ version = "0.12.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tek_api"
version = "0.1.0"
dependencies = [
"tek_core",
"uuid",
]
[[package]]
name = "tek_app"
version = "0.1.0"
dependencies = [
"livi",
"suil-rs",
"symphonia",
"tek_api",
"tek_core",
"vst",
"wavers",
"winit",
]
[[package]] [[package]]
name = "tek_core" name = "tek_core"
version = "0.1.0" version = "0.1.0"

View file

@ -2,6 +2,6 @@
resolver = "2" resolver = "2"
members = [ members = [
"crates/tek_core", "crates/tek_core",
"crates/tek_mixer", "crates/tek_api",
"crates/tek_sequencer" "crates/tek_app"
] ]

View file

@ -0,0 +1,8 @@
[package]
name = "tek_api"
edition = "2021"
version = "0.1.0"
[dependencies]
tek_core = { path = "../tek_core" }
uuid = { version = "1.10.0", features = [ "v4" ] }

View file

@ -0,0 +1,143 @@
use crate::*;
#[derive(Clone)]
pub enum ArrangerCommand {
Focus(FocusCommand),
Transport(TransportCommand),
Phrases(PhrasePoolCommand),
Editor(PhraseEditorCommand),
Arrangement(ArrangementCommand),
EditPhrase(Option<Arc<RwLock<Phrase>>>),
}
#[derive(Clone)]
pub enum ArrangementCommand {
New,
Load,
Save,
ToggleViewMode,
Delete,
Activate,
Increment,
Decrement,
ZoomIn,
ZoomOut,
Go(Direction),
Edit(Option<Arc<RwLock<Phrase>>>),
Scene(SceneCommand),
Track(TrackCommand),
Clip(ClipCommand),
}
#[derive(Clone)]
pub enum SceneCommand {
Next,
Prev,
Add,
Delete,
MoveForward,
MoveBack,
RandomColor,
SetSize(usize),
SetZoom(usize),
}
#[derive(Clone)]
pub enum TrackCommand {
Next,
Prev,
Add,
Delete,
MoveForward,
MoveBack,
RandomColor,
SetSize(usize),
SetZoom(usize),
}
#[derive(Clone)]
pub enum ClipCommand {
SetLoop(bool),
Get(usize, usize),
Put(usize, usize, Option<Arc<RwLock<Phrase>>>),
Edit(Option<Arc<RwLock<Phrase>>>),
}
#[derive(Clone, PartialEq)]
pub enum SequencerCommand {
Focus(FocusCommand),
Transport(TransportCommand),
Phrases(PhrasePoolCommand),
Editor(PhraseEditorCommand),
}
#[derive(Clone, PartialEq)]
pub enum PhrasePoolCommand {
Prev,
Next,
MoveUp,
MoveDown,
Delete,
Append,
Insert,
Duplicate,
RandomColor,
Edit,
Import,
Export,
Rename(PhraseRenameCommand),
Length(PhraseLengthCommand),
}
#[derive(Clone, PartialEq)]
pub enum PhraseRenameCommand {
Begin,
Backspace,
Append(char),
Set(String),
Confirm,
Cancel,
}
#[derive(Clone, PartialEq)]
pub enum PhraseLengthCommand {
Begin,
Next,
Prev,
Inc,
Dec,
Set(usize),
Confirm,
Cancel,
}
#[derive(Clone, PartialEq)]
pub enum PhraseEditorCommand {
// TODO: 1-9 seek markers that by default start every 8th of the phrase
ToggleDirection,
EnterEditMode,
ExitEditMode,
NoteAppend,
NoteSet,
NoteCursorSet(usize),
NoteLengthSet(usize),
NoteScrollSet(usize),
TimeCursorSet(usize),
TimeScrollSet(usize),
TimeZoomSet(usize),
Go(Direction),
}
#[derive(Copy, Clone, PartialEq)]
pub enum TransportCommand {
FocusNext,
FocusPrev,
Play(Option<usize>),
Pause(Option<usize>),
SeekUsec(f64),
SeekSample(f64),
SeekPulse(f64),
SetBpm(f64),
SetQuant(f64),
SetSync(f64),
}

View file

@ -0,0 +1,158 @@
use crate::*;
impl Scene {
pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually<Self> {
let mut name = None;
let mut clips = vec![];
edn!(edn in args {
Edn::Map(map) => {
let key = map.get(&Edn::Key(":name"));
if let Some(Edn::Str(n)) = key {
name = Some(*n);
} else {
panic!("unexpected key in scene '{name:?}': {key:?}")
}
},
Edn::Symbol("_") => {
clips.push(None);
},
Edn::Int(i) => {
clips.push(Some(*i as usize));
},
_ => panic!("unexpected in scene '{name:?}': {edn:?}")
});
let scene = Self::new(name.unwrap_or(""), clips);
Ok(scene)
}
}
const SYM_NAME: &'static str = ":name";
const SYM_GAIN: &'static str = ":gain";
const SYM_SAMPLER: &'static str = "sampler";
const SYM_LV2: &'static str = "lv2";
impl<E: Engine> Track<E> {
pub fn from_edn <'a, 'e> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<Self> {
let mut _gain = 0.0f64;
let mut track = Self::new("")?;
#[allow(unused_mut)]
let mut devices: Vec<JackDevice<E>> = vec![];
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(SYM_NAME)) {
track.name = n.to_string();
}
if let Some(Edn::Double(g)) = map.get(&Edn::Key(SYM_GAIN)) {
_gain = f64::from(*g);
}
},
Edn::List(args) => match args.get(0) {
// Add a sampler device to the track
Some(Edn::Symbol(SYM_SAMPLER)) => {
devices.push(Sampler::from_edn(jack, &args[1..])?);
panic!(
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"sampler\"",
&track.name,
args.get(0).unwrap()
)
},
// Add a LV2 plugin to the track.
Some(Edn::Symbol(SYM_LV2)) => {
devices.push(LV2Plugin::from_edn(jack, &args[1..])?);
panic!(
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"plugin\"",
&track.name,
args.get(0).unwrap()
)
},
None =>
panic!("empty list track {}", &track.name),
_ =>
panic!("unexpected in track {}: {:?}", &track.name, args.get(0).unwrap())
},
_ => {}
});
for device in devices {
track.add_device(device);
}
Ok(track)
}
}
impl LV2Plugin {
pub fn from_edn <'e, E: Engine> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<JackDevice<E>> {
let mut name = String::new();
let mut path = String::new();
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
name = String::from(*n);
}
if let Some(Edn::Str(p)) = map.get(&Edn::Key(":path")) {
path = String::from(*p);
}
},
_ => panic!("unexpected in lv2 '{name}'"),
});
Plugin::new_lv2(jack, &name, &path)
}
}
impl<E: Engine> Sampler<E> {
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<JackDevice<E>> {
let mut name = String::new();
let mut dir = String::new();
let mut samples = BTreeMap::new();
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
name = String::from(*n);
}
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) {
dir = String::from(*n);
}
},
Edn::List(args) => match args.get(0) {
Some(Edn::Symbol("sample")) => {
let (midi, sample) = Sample::from_edn(jack, &dir, &args[1..])?;
if let Some(midi) = midi {
samples.insert(midi, sample);
} else {
panic!("sample without midi binding: {}", sample.read().unwrap().name);
}
},
_ => panic!("unexpected in sampler {name}: {args:?}")
},
_ => panic!("unexpected in sampler {name}: {edn:?}")
});
Self::new(jack, &name, Some(samples))
}
}
impl Sample {
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, dir: &str, args: &[Edn<'e>]) -> Usually<(Option<u7>, Arc<RwLock<Self>>)> {
let mut name = String::new();
let mut file = String::new();
let mut midi = None;
let mut start = 0usize;
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
name = String::from(*n);
}
if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) {
file = String::from(*f);
}
if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) {
start = *i as usize;
}
if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) {
midi = Some(u7::from(*m as u8));
}
},
_ => panic!("unexpected in sample {name}"),
});
let (end, data) = read_sample_data(&format!("{dir}/{file}"))?;
Ok((midi, Arc::new(RwLock::new(Self::new(&name, start, end, data)))))
}
}

View file

@ -0,0 +1,28 @@
use crate::*;
pub(crate) use tek_core::midly::MidiMessage;
/// MIDI message structural
pub type PhraseData = Vec<Vec<MidiMessage>>;
/// A MIDI sequence.
#[derive(Debug, Clone)] pub struct Phrase {
pub uuid: uuid::Uuid,
/// Name of phrase
pub name: String,
/// Temporal resolution in pulses per quarter note
pub ppq: usize,
/// Length of phrase in pulses
pub length: usize,
/// Notes in phrase
pub notes: PhraseData,
/// Whether to loop the phrase or play it once
pub loop_on: bool,
/// Start of loop
pub loop_start: usize,
/// Length of loop
pub loop_length: usize,
/// All notes are displayed with minimum length
pub percussive: bool,
/// Identifying color of phrase
pub color: ItemColorTriplet,
}

View file

@ -0,0 +1,6 @@
pub(crate) use tek_core::*;
submod! {
api_cmd
api_edn
api_midi
}

View file

@ -1,10 +1,11 @@
[package] [package]
name = "tek_mixer" name = "tek_app"
edition = "2021" edition = "2021"
version = "0.1.0" version = "0.1.0"
[dependencies] [dependencies]
tek_core = { path = "../tek_core" } tek_core = { path = "../tek_core" }
tek_api = { path = "../tek_api" }
livi = "0.7.4" livi = "0.7.4"
suil-rs = { path = "../suil" } suil-rs = { path = "../suil" }
@ -32,3 +33,15 @@ path = "src/sampler_cli.rs"
[[bin]] [[bin]]
name = "tek_plugin" name = "tek_plugin"
path = "src/plugin_cli.rs" path = "src/plugin_cli.rs"
[[bin]]
name = "tek_sequencer"
path = "src/sequencer_cli.rs"
[[bin]]
name = "tek_arranger"
path = "src/arranger_cli.rs"
[[bin]]
name = "tek_transport"
path = "src/transport_cli.rs"

View file

@ -21,3 +21,25 @@ This crate implements time sync and JACK transport control.
* Todo: edit numeric values * Todo: edit numeric values
* Todo: jump to time/bbt markers * Todo: jump to time/bbt markers
* Todo: count xruns * Todo: count xruns
---
# `tek_mixer`
// TODO:
// - Meters: propagate clipping:
// - If one stage clips, all stages after it are marked red
// - If one track clips, all tracks that feed from it are marked red?
# `tek_track`
---
# `tek_sampler`
This crate implements a sampler device which plays audio files
in response to MIDI notes.
---
# `tek_plugin`

View file

@ -9,7 +9,7 @@ pub enum ArrangerCommand {
Arrangement(ArrangementCommand), Arrangement(ArrangementCommand),
EditPhrase(Option<Arc<RwLock<Phrase>>>), EditPhrase(Option<Arc<RwLock<Phrase>>>),
} }
#[derive(Clone, PartialEq)] #[derive(Clone)]
pub enum ArrangementCommand { pub enum ArrangementCommand {
New, New,
Load, Load,
@ -33,7 +33,7 @@ pub enum ArrangementCommand {
GoDown, GoDown,
GoLeft, GoLeft,
GoRight, GoRight,
Edit, Edit(Option<Arc<RwLock<Phrase>>>),
} }
impl<E: Engine> Command<Arranger<E>> for ArrangerCommand { impl<E: Engine> Command<Arranger<E>> for ArrangerCommand {
@ -75,7 +75,7 @@ impl<E: Engine> Command<Arrangement<E>> for ArrangementCommand {
New => todo!(), New => todo!(),
Load => todo!(), Load => todo!(),
Save => todo!(), Save => todo!(),
Edit => todo!(), Edit(phrase) => { state.phrase = phrase.clone() },
ToggleViewMode => { state.mode.to_next(); }, ToggleViewMode => { state.mode.to_next(); },
Delete => { state.delete(); }, Delete => { state.delete(); },
Activate => { state.activate(); }, Activate => { state.activate(); },

View file

@ -78,7 +78,7 @@ impl Handle<Tui> for Arrangement<Tui> {
} }
impl InputToCommand<Tui, Arrangement<Tui>> for ArrangementCommand { impl InputToCommand<Tui, Arrangement<Tui>> for ArrangementCommand {
fn input_to_command (_: &Arrangement<Tui>, input: &TuiInput) -> Option<Self> { fn input_to_command (state: &Arrangement<Tui>, input: &TuiInput) -> Option<Self> {
use ArrangementCommand::*; use ArrangementCommand::*;
match input.event() { match input.event() {
key!(KeyCode::Char('`')) => Some(ToggleViewMode), key!(KeyCode::Char('`')) => Some(ToggleViewMode),
@ -95,7 +95,7 @@ impl InputToCommand<Tui, Arrangement<Tui>> for ArrangementCommand {
key!(KeyCode::Char('c')) => Some(RandomColor), key!(KeyCode::Char('c')) => Some(RandomColor),
key!(KeyCode::Char('s')) => Some(Put), key!(KeyCode::Char('s')) => Some(Put),
key!(KeyCode::Char('g')) => Some(Get), key!(KeyCode::Char('g')) => Some(Get),
key!(KeyCode::Char('e')) => Some(Edit), key!(KeyCode::Char('e')) => Some(Edit(state.phrase())),
key!(Ctrl-KeyCode::Char('a')) => Some(AddScene), key!(Ctrl-KeyCode::Char('a')) => Some(AddScene),
key!(Ctrl-KeyCode::Char('t')) => Some(AddTrack), key!(Ctrl-KeyCode::Char('t')) => Some(AddTrack),
key!(KeyCode::Char('l')) => Some(ToggleLoop), key!(KeyCode::Char('l')) => Some(ToggleLoop),

45
crates/tek_app/src/lib.rs Normal file
View file

@ -0,0 +1,45 @@
pub(crate) use tek_core::*;
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
pub(crate) use tek_core::midly::{num::u7, live::LiveEvent, MidiMessage};
pub(crate) use tek_core::jack::*;
pub(crate) use std::collections::BTreeMap;
pub(crate) use std::sync::{Arc, Mutex, RwLock};
pub(crate) use std::path::PathBuf;
pub(crate) use std::ffi::OsString;
pub(crate) use std::fs::read_dir;
submod! {
arranger
arranger_cmd
arranger_snd
arranger_tui
arranger_tui_bar
arranger_tui_cmd
arranger_tui_col
arranger_tui_hor
arranger_tui_ver
sequencer
sequencer_cmd
sequencer_snd
sequencer_tui
transport
transport_cmd
transport_snd
transport_tui
mixer
mixer_snd
mixer_cmd
mixer_tui
sampler
sampler_snd
sampler_cmd
plugin
plugin_snd
plugin_cmd
plugin_tui
plugin_lv2
plugin_lv2_gui
plugin_vst2
plugin_vst3
}

View file

@ -1,15 +0,0 @@
# `tek_mixer`
// TODO:
// - Meters: propagate clipping:
// - If one stage clips, all stages after it are marked red
// - If one track clips, all tracks that feed from it are marked red?
# `tek_track`
# `tek_sampler`
This crate implements a sampler device which plays audio files
in response to MIDI notes.
# `tek_plugin`

View file

@ -1,16 +0,0 @@
pub(crate) use tek_core::*;
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
pub(crate) use tek_core::midly::{num::u7, live::LiveEvent, MidiMessage};
pub(crate) use tek_core::jack::*;
pub(crate) use std::collections::BTreeMap;
pub(crate) use std::sync::{Arc, Mutex, RwLock};
pub(crate) use std::path::PathBuf;
pub(crate) use std::ffi::OsString;
pub(crate) use std::fs::read_dir;
submod! {
mixer mixer_snd mixer_edn mixer_cmd mixer_tui
sampler sampler_snd sampler_edn sampler_cmd
plugin plugin_snd plugin_edn plugin_cmd plugin_tui plugin_lv2 plugin_lv2_gui plugin_vst2 plugin_vst3
}

View file

@ -1,54 +0,0 @@
use crate::*;
const SYM_NAME: &'static str = ":name";
const SYM_GAIN: &'static str = ":gain";
const SYM_SAMPLER: &'static str = "sampler";
const SYM_LV2: &'static str = "lv2";
impl<E: Engine> Track<E> {
pub fn from_edn <'a, 'e> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<Self> {
let mut _gain = 0.0f64;
let mut track = Self::new("")?;
#[allow(unused_mut)]
let mut devices: Vec<JackDevice<E>> = vec![];
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(SYM_NAME)) {
track.name = n.to_string();
}
if let Some(Edn::Double(g)) = map.get(&Edn::Key(SYM_GAIN)) {
_gain = f64::from(*g);
}
},
Edn::List(args) => match args.get(0) {
// Add a sampler device to the track
Some(Edn::Symbol(SYM_SAMPLER)) => {
devices.push(Sampler::from_edn(jack, &args[1..])?);
panic!(
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"sampler\"",
&track.name,
args.get(0).unwrap()
)
},
// Add a LV2 plugin to the track.
Some(Edn::Symbol(SYM_LV2)) => {
devices.push(LV2Plugin::from_edn(jack, &args[1..])?);
panic!(
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"plugin\"",
&track.name,
args.get(0).unwrap()
)
},
None =>
panic!("empty list track {}", &track.name),
_ =>
panic!("unexpected in track {}: {:?}", &track.name, args.get(0).unwrap())
},
_ => {}
});
for device in devices {
track.add_device(device);
}
Ok(track)
}
}

View file

@ -1,20 +0,0 @@
use crate::*;
impl LV2Plugin {
pub fn from_edn <'e, E: Engine> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<JackDevice<E>> {
let mut name = String::new();
let mut path = String::new();
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
name = String::from(*n);
}
if let Some(Edn::Str(p)) = map.get(&Edn::Key(":path")) {
path = String::from(*p);
}
},
_ => panic!("unexpected in lv2 '{name}'"),
});
Plugin::new_lv2(jack, &name, &path)
}
}

View file

@ -1,60 +0,0 @@
use crate::*;
impl<E: Engine> Sampler<E> {
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<JackDevice<E>> {
let mut name = String::new();
let mut dir = String::new();
let mut samples = BTreeMap::new();
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
name = String::from(*n);
}
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) {
dir = String::from(*n);
}
},
Edn::List(args) => match args.get(0) {
Some(Edn::Symbol("sample")) => {
let (midi, sample) = Sample::from_edn(jack, &dir, &args[1..])?;
if let Some(midi) = midi {
samples.insert(midi, sample);
} else {
panic!("sample without midi binding: {}", sample.read().unwrap().name);
}
},
_ => panic!("unexpected in sampler {name}: {args:?}")
},
_ => panic!("unexpected in sampler {name}: {edn:?}")
});
Self::new(jack, &name, Some(samples))
}
}
impl Sample {
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, dir: &str, args: &[Edn<'e>]) -> Usually<(Option<u7>, Arc<RwLock<Self>>)> {
let mut name = String::new();
let mut file = String::new();
let mut midi = None;
let mut start = 0usize;
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
name = String::from(*n);
}
if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) {
file = String::from(*f);
}
if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) {
start = *i as usize;
}
if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) {
midi = Some(u7::from(*m as u8));
}
},
_ => panic!("unexpected in sample {name}"),
});
let (end, data) = read_sample_data(&format!("{dir}/{file}"))?;
Ok((midi, Arc::new(RwLock::new(Self::new(&name, start, end, data)))))
}
}

View file

@ -1,23 +0,0 @@
[package]
name = "tek_sequencer"
edition = "2021"
version = "0.1.0"
[dependencies]
tek_core = { path = "../tek_core" }
uuid = { version = "1.10.0", features = [ "v4" ] }
[lib]
path = "src/lib.rs"
[[bin]]
name = "tek_sequencer"
path = "src/sequencer_cli.rs"
[[bin]]
name = "tek_arranger"
path = "src/arranger_cli.rs"
[[bin]]
name = "tek_transport"
path = "src/transport_cli.rs"

View file

@ -1,27 +0,0 @@
use crate::*;
impl Scene {
pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually<Self> {
let mut name = None;
let mut clips = vec![];
edn!(edn in args {
Edn::Map(map) => {
let key = map.get(&Edn::Key(":name"));
if let Some(Edn::Str(n)) = key {
name = Some(*n);
} else {
panic!("unexpected key in scene '{name:?}': {key:?}")
}
},
Edn::Symbol("_") => {
clips.push(None);
},
Edn::Int(i) => {
clips.push(Some(*i as usize));
},
_ => panic!("unexpected in scene '{name:?}': {edn:?}")
});
let scene = Self::new(name.unwrap_or(""), clips);
Ok(scene)
}
}

View file

@ -1,32 +0,0 @@
/// Phrase editor.
pub(crate) use tek_core::*;
pub(crate) use tek_core::crossterm::event::KeyCode;
pub(crate) use tek_core::midly::{num::u7, live::LiveEvent, MidiMessage};
pub(crate) use tek_core::jack::*;
pub(crate) use std::sync::{Arc, RwLock};
submod! {
arranger
arranger_cmd
arranger_snd
arranger_tui
arranger_tui_bar
arranger_tui_cmd
arranger_tui_col
arranger_tui_hor
arranger_tui_ver
sequencer
sequencer_cmd
sequencer_snd
sequencer_tui
transport
transport_cmd
transport_snd
transport_tui
}
/// Octave number (from -1 to 9)
pub const NTH_OCTAVE: [&'static str;11] = [
"-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
];