Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/src/API/cycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
> * Stacks and random choices are valid without brackets (`a | b` is parsed as `[a | b]`)
> * `:` sets the instrument or remappable target instead of selecting samples but also
> allows setting note attributes such as instrument/volume/pan/delay (e.g. `c4:v0.1:p0.5`)
> * In bjorklund expressions, operators *within* and on the *right side* are not supported
> (e.g. `bd(<3 2>, 8)` and `bd(3, 8)*2` are *not* supported)
> * In bjorklund expressions, operators *within* are not supported
> (e.g. `bd(<3 2>, 8)` is *not* supported)
>
> [Tidal Cycles Reference](https://tidalcycles.org/docs/reference/mini_notation/)
>
Expand Down
28 changes: 25 additions & 3 deletions docs/src/guide/cycles.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ There's no exact specification for how tidal cycles work, and it's constantly ev

* `:` sets the instrument or remappable target instead of selecting samples but also allows setting note attributes such as instrument/volume/pan/delay (e.g. `c4:v0.1:p0.5`)

* In bjorklund expressions, operators *within* and on the *right side* are not supported (e.g. `bd(<3 2>, 8)` and `bd(3, 8)*2` are *not* supported)
* In bjorklund expressions, operators *within* are not supported (e.g. `bd(<3 2>, 8)` is *not* supported)

### Timing

Expand All @@ -86,7 +86,7 @@ The base time of a pattern in tidal is specified as *cycles per second*. In afse
-- emits an entire cycle every beat
return rhythm {
unit = "beats",
emit = cycle("c4 d4 e4") -- tripplet
emit = cycle("c4 d4 e4") -- triplet
}
```

Expand Down Expand Up @@ -141,6 +141,28 @@ Supported note attributes are:

Note that `X` must be written as *floating point number* for volume, panning and delay:</br> `c4:p-1.0` and `c4:p.8` is valid, while `c4:p-1` **is not valid**!

If you want to use expressions (like slowing down) for an attribute pattern on the right side, you'll have to wrap it in square brackets, otherwise the expression applies to the entire pattern, not just the attributes'.

```lua
-- This slows down the output
cycle("[c4 d#4 e4]:<v.1 v.2>/2")

-- This slows down the alternating for the volume
cycle("[c4 d#4 e4]:[<v.1 v.2>/2]")

```

A shorthand for assigning attributes exists in the form of `:v=X` where `X` can be a pattern. This way, you can supply float values without having to repeat the name of the target attribute.

```lua
-- Set volume to rise for each cycle
cycle("[c4 d#4 e4]:v=<.1 .2 .3 .4>")

-- This would be the same as
cycle("[c4 d#4 e4]:<v.1 v.2 v.3 v.4>")
```


### Mapping

Notes and chords in cycles are expressed as [note strings](./notes&scales.md#note-strings) in afseq. But you can also dynamically evaluate and map cycle identifiers using the cycle [`map`](../API/cycle.md#map) function.
Expand Down Expand Up @@ -224,4 +246,4 @@ return rhythm {
end
)
}
```
```
59 changes: 25 additions & 34 deletions src/event/cycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ use std::{collections::HashMap, ops::RangeBounds};
type Fraction = num_rational::Rational32;

use crate::{
event::new_note, BeatTimeBase, Chord, Cycle, CycleEvent, CyclePropertyKey, CyclePropertyValue,
CycleTarget, CycleValue, Event, EventIter, EventIterItem, InputParameterSet, InstrumentId,
Note, NoteEvent, PulseIterItem,
event::new_note, BeatTimeBase, Chord, Cycle, CycleEvent, CycleTarget, CycleValue, Event,
EventIter, EventIterItem, InputParameterSet, InstrumentId, Note, NoteEvent, PulseIterItem,
};

// -------------------------------------------------------------------------------------------------
Expand All @@ -31,7 +30,7 @@ impl TryFrom<&CycleValue> for Vec<Option<NoteEvent>> {
.map(|i| new_note(chord.note().transposed(*i as i32)))
.collect())
}
CycleValue::Property(_, _) => Ok(vec![None]),
CycleValue::Target(_) => Ok(vec![None]),
CycleValue::Name(s) => {
if s.eq_ignore_ascii_case("off") {
Ok(vec![new_note(Note::OFF)])
Expand All @@ -47,37 +46,36 @@ impl TryFrom<&CycleValue> for Vec<Option<NoteEvent>> {

// Conversion helpers for cycle targets
fn float_value_in_range<Range>(
property: &CyclePropertyValue,
maybe_float: &Option<f64>,
name: &'static str,
range: Range,
) -> Result<f32, String>
where
Range: RangeBounds<f32> + std::fmt::Debug,
{
let value = property
.to_float()
.ok_or(format!("{} property must be a number value", name))? as f32;
if range.contains(&value) {
Ok(value)
} else {
Err(format!(
"{} property must be in range [{:?}] but is '{}'",
name, range, value
))
}
maybe_float
.map(|v| v as f32)
.ok_or_else(|| format!("{} property must be a number value", name))
.and_then(|v| {
if range.contains(&v) {
Ok(v)
} else {
Err(format!(
"{} property must be in range [{:?}] but is '{}'",
name, range, v
))
}
})
}

fn integer_value_in_range<Range>(
property: &CyclePropertyValue,
value: i32,
name: &'static str,
range: Range,
) -> Result<i32, String>
where
Range: RangeBounds<i32> + std::fmt::Debug,
{
let value = property
.to_integer()
.ok_or(format!("{} property must be an integer value", name))?;
if range.contains(&value) {
Ok(value)
} else {
Expand All @@ -99,10 +97,10 @@ pub(crate) fn apply_cycle_note_properties(
}
// apply for all non empty note events
for target in targets {
match target.key() {
CyclePropertyKey::Index(index) => {
match target {
CycleTarget::Index(index) => {
let index = integer_value_in_range(
&CyclePropertyValue::Integer(*index),
*index,
"instrument",
0..,
)?;
Expand All @@ -111,29 +109,22 @@ pub(crate) fn apply_cycle_note_properties(
note_event.instrument = Some(instrument);
}
}
CyclePropertyKey::Name(name) => {
CycleTarget::Named(name, value) => {
match name.as_bytes() {
b"#" => {
let index = integer_value_in_range(target.value(), "instrument", 0..)?;
let instrument = InstrumentId::from(index as usize);
for note_event in note_events.iter_mut().flatten() {
note_event.instrument = Some(instrument);
}
}
b"v" => {
let volume = float_value_in_range(target.value(), "volume", 0.0..=1.0)?;
let volume = float_value_in_range(value, "volume", 0.0..=1.0)?;
for note_event in note_events.iter_mut().flatten() {
note_event.volume = volume;
}
}
b"p" => {
let panning = float_value_in_range(target.value(), "panning", -1.0..=1.0)?;
let panning = float_value_in_range(value, "panning", -1.0..=1.0)?;
for note_event in note_events.iter_mut().flatten() {
note_event.panning = panning;
}
}
b"d" => {
let delay = float_value_in_range(target.value(), "delay", 0.0..1.0)?;
let delay = float_value_in_range(value, "delay", 0.0..1.0)?;
for note_event in note_events.iter_mut().flatten() {
note_event.delay = delay;
}
Expand Down
4 changes: 1 addition & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,7 @@ pub use event::{

pub mod tidal;
pub use tidal::{
Cycle, Event as CycleEvent, PropertyKey as CyclePropertyKey,
PropertyValue as CyclePropertyValue, Span as CycleSpan, Target as CycleTarget,
Value as CycleValue,
Cycle, Event as CycleEvent, Span as CycleSpan, Target as CycleTarget, Value as CycleValue,
};

pub mod pulse;
Expand Down
2 changes: 0 additions & 2 deletions src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ pub use super::{
Chord,
Cycle,
CycleEvent,
CyclePropertyKey,
CyclePropertyValue,
CycleSpan,
CycleTarget,
CycleValue,
Expand Down
2 changes: 1 addition & 1 deletion src/tidal.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Tidal mini parser and event generator, used as `EventIter`.

mod cycle;
pub use cycle::{Cycle, Event, Pitch, PropertyKey, PropertyValue, Span, Target, Value};
pub use cycle::{Cycle, Event, Pitch, Span, Target, Value};
11 changes: 7 additions & 4 deletions src/tidal/cycle.pest
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ mark = { "#"|"b" }
note = ${ (^"a"|^"b"|^"c"|^"d"|^"e"|^"f"|^"g") }
pitch = ${ note ~ mark? ~ octave? ~ !name}

// target properties such as volume "v0.1" (afseq extension)
property = ${ ("#" ~ integer) | (ASCII_ALPHA ~ float) }
/// target properties such as volume "v0.1" (afseq extension)
target = ${ ("#" ~ integer) | (ASCII_ALPHA ~ float) }
/// patterns for assigning target keys with pattern on the right side
target_name = ${ "#" | name }
target_assign = { target_name ~ "=" ~ parameter }

/// chord as pitch with mode string, separated via "'"
mode = ${ (ASCII_ALPHANUMERIC | "#" | "-" | "+" | "^")+ }
Expand All @@ -34,7 +37,7 @@ name = @{ ASCII_ALPHANUMERIC ~ (ASCII_ALPHANUMERIC | "_")* }
repeat = { "!" }

/// possible literals for single steps
single = { hold | rest | chord | property | pitch | number | name }
single = { hold | rest | chord | target | pitch | number | name }

choice_op = {"|"}
stack_op = {","}
Expand Down Expand Up @@ -63,7 +66,7 @@ op_degrade = ${ "?" ~ number? }
/// dynamic operators
op_fast = { "*" ~ parameter }
op_slow = { "/" ~ parameter }
op_target = { ":" ~ parameter }
op_target = { ":" ~ (target_assign | parameter) }
// this should actually use `parameter` as well once bjorklund with patterns on the right is implemented
op_bjorklund = { "(" ~ (single_parameter ~ ",")+ ~ single_parameter ~ ")" }

Expand Down
Loading