• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mbarbin / fingerboard / 116

23 Dec 2025 04:57PM UTC coverage: 95.087% (-0.5%) from 95.592%
116

push

github

mbarbin
Use Code_error.raise

40 of 84 new or added lines in 8 files covered. (47.62%)

77 existing lines in 10 files now uncovered.

3716 of 3908 relevant lines covered (95.09%)

18681.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.27
/src/system.ml
1
(**********************************************************************************)
2
(*  Fingerboard - a microtonal geography of the cello fingerboard                 *)
3
(*  Copyright (C) 2022-2024 Mathieu Barbin <mathieu.barbin@gmail.com>             *)
4
(*                                                                                *)
5
(*  This file is part of Fingerboard.                                             *)
6
(*                                                                                *)
7
(*  Fingerboard is free software: you can redistribute it and/or modify it under  *)
8
(*  the terms of the GNU Affero General Public License as published by the Free   *)
9
(*  Software Foundation, either version 3 of the License, or any later version.   *)
10
(*                                                                                *)
11
(*  Fingerboard is distributed in the hope that it will be useful, but WITHOUT    *)
12
(*  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or         *)
13
(*  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License   *)
14
(*  for more details.                                                             *)
15
(*                                                                                *)
16
(*  You should have received a copy of the GNU Affero General Public License      *)
17
(*  along with Fingerboard. If not, see <https://www.gnu.org/licenses/>.          *)
18
(**********************************************************************************)
19

20
module Vibrating_string : sig
21
  type t =
22
    { open_string : Note.t
23
    ; mutable pitch : Frequency.t
24
    ; roman_numeral : Roman_numeral.t
25
    }
26

27
  val to_dyn : t -> Dyn.t
28
end = struct
29
  type t =
30
    { open_string : Note.t
31
    ; mutable pitch : Frequency.t
32
    ; roman_numeral : Roman_numeral.t
33
    }
34

35
  let to_dyn { open_string; pitch; roman_numeral } =
36
    Dyn.record
61✔
37
      [ "open_string", open_string |> Note.to_dyn
61✔
38
      ; "pitch", pitch |> Frequency.to_dyn
61✔
39
      ; "roman_numeral", roman_numeral |> Roman_numeral.to_dyn
61✔
40
      ]
41
  ;;
42
end
43

44
type t =
45
  { vibrating_strings : Vibrating_string.t array
46
  ; intervals_going_down : Characterized_interval.t array
47
  ; mutable fingerboard_positions : Fingerboard_position.t list
48
  }
49

50
let to_dyn { vibrating_strings; intervals_going_down; fingerboard_positions } =
51
  Dyn.record
15✔
52
    (List.concat
15✔
53
       [ [ "vibrating_strings", vibrating_strings |> Dyn.array Vibrating_string.to_dyn
15✔
54
         ; ( "intervals_going_down"
55
           , intervals_going_down |> Dyn.array Characterized_interval.to_dyn )
15✔
56
         ]
57
       ; (if List.is_empty fingerboard_positions
58
          then []
8✔
59
          else
60
            [ ( "fingerboard_positions"
7✔
61
              , fingerboard_positions |> Dyn.list Fingerboard_position.to_dyn )
7✔
62
            ])
63
       ])
64
;;
65

66
let to_ascii_tables { vibrating_strings; intervals_going_down; fingerboard_positions } =
67
  let vibrating_strings =
7✔
68
    let columns =
69
      Print_table.O.
70
        [ Column.make
7✔
71
            ~align:Right
72
            ~header:"String"
73
            (fun (_, { Vibrating_string.open_string = _; pitch = _; roman_numeral }) ->
74
               Cell.text (Roman_numeral.to_string roman_numeral))
28✔
75
        ; Column.make ~header:"Note" (fun (_, (t : Vibrating_string.t)) ->
7✔
76
            Cell.text (Note.to_string t.open_string))
28✔
77
        ; Column.make ~align:Right ~header:"Pitch" (fun (_, (t : Vibrating_string.t)) ->
7✔
78
            Cell.text (Printf.sprintf "%0.2f" (Frequency.to_float t.pitch)))
28✔
79
        ; Column.make ~header:"Interval" (fun (i, _) ->
7✔
80
            if i >= Array.length intervals_going_down
28✔
81
            then Cell.empty
7✔
82
            else (
21✔
83
              let { Characterized_interval.interval; acoustic_interval } =
84
                intervals_going_down.(i)
85
              in
86
              Cell.text
21✔
87
                (Printf.sprintf
21✔
88
                   "%s - %s"
89
                   (Interval.to_string interval)
21✔
90
                   (Acoustic_interval.to_string acoustic_interval))))
21✔
91
        ; Column.make ~align:Right ~header:"Cents" (fun (i, _) ->
7✔
92
            if i >= Array.length intervals_going_down
28✔
93
            then Cell.empty
7✔
94
            else (
21✔
95
              let { Characterized_interval.acoustic_interval; _ } =
96
                intervals_going_down.(i)
97
              in
98
              Cell.text
21✔
99
                (Cents.to_string_nearest (Acoustic_interval.to_cents acoustic_interval))))
21✔
100
        ]
101
    in
102
    Print_table.make
103
      ~columns
104
      ~rows:(Array.to_list vibrating_strings |> List.mapi ~f:(fun i v -> i, v))
7✔
105
  and fingerboard_positions =
106
    Print_table.make
107
      ~columns:Fingerboard_position.ascii_table_columns
108
      ~rows:fingerboard_positions
109
  in
110
  [ vibrating_strings; fingerboard_positions ]
111
  |> List.map ~f:Print_table.to_string_text
112
  |> String.concat ~sep:"\n"
7✔
113
;;
114

115
let create ~high_vibrating_string ~pitch ~intervals_going_down =
116
  let high_vibrating_string =
117✔
117
    { Vibrating_string.open_string = high_vibrating_string
118
    ; pitch
119
    ; roman_numeral = Roman_numeral.one
120
    }
121
  in
122
  let other_strings =
123
    Array.fold_map
124
      intervals_going_down
125
      ~init:high_vibrating_string
126
      ~f:(fun previous_string { Characterized_interval.interval; acoustic_interval } ->
127
        let v =
352✔
128
          { Vibrating_string.open_string =
129
              previous_string.open_string
130
              |> Interval.shift_down interval
131
              |> Option.value_exn ~here:[%here]
352✔
132
          ; pitch =
133
              previous_string.pitch |> Acoustic_interval.shift_down acoustic_interval
352✔
134
          ; roman_numeral = Roman_numeral.succ_exn previous_string.roman_numeral
352✔
135
          }
136
        in
137
        v, v)
138
    |> snd
117✔
139
  in
140
  { vibrating_strings = Array.concat [ [| high_vibrating_string |]; other_strings ]
117✔
141
  ; intervals_going_down
142
  ; fingerboard_positions = []
143
  }
144
;;
145

146
let reset_pitch t roman_numeral ~pitch =
147
  let index = Roman_numeral.to_int roman_numeral |> Int.pred in
3✔
148
  t.vibrating_strings.(index).pitch <- pitch;
3✔
149
  (* Tune going up. *)
150
  for i = index - 1 downto 0 do
151
    t.vibrating_strings.(i).pitch
1✔
152
    <- t.vibrating_strings.(i + 1).pitch
153
       |> Acoustic_interval.shift_up t.intervals_going_down.(i).acoustic_interval
1✔
154
  done;
155
  (* Tune going down. *)
156
  for i = index + 1 to Array.length t.vibrating_strings - 1 do
3✔
157
    t.vibrating_strings.(i).pitch
8✔
158
    <- t.vibrating_strings.(i - 1).pitch
159
       |> Acoustic_interval.shift_down t.intervals_going_down.(i - 1).acoustic_interval
8✔
160
  done;
161
  ()
162
;;
163

164
let vibrating_string_exn (t : t) string_number =
165
  let index = Roman_numeral.to_int string_number - 1 in
1,320,346✔
UNCOV
166
  if index < 0 || index >= Array.length t.vibrating_strings
×
UNCOV
167
  then (
×
UNCOV
168
    let available = Array.map t.vibrating_strings ~f:(fun t -> t.roman_numeral) in
×
NEW
169
    Code_error.raise
×
170
      "String number out of bounds."
NEW
171
      [ "string_number", string_number |> Roman_numeral.to_dyn
×
NEW
172
      ; "available", available |> Dyn.array Roman_numeral.to_dyn
×
173
      ])
174
  else t.vibrating_strings.(index)
1,320,346✔
175
;;
176

177
let pitch (t : t) { Fingerboard_location.fingerboard_position; string_number } =
178
  let vibrating_string = vibrating_string_exn t string_number in
32✔
179
  let interval =
32✔
180
    Fingerboard_position.acoustic_interval_to_the_open_string fingerboard_position
181
  in
182
  Acoustic_interval.shift_up interval vibrating_string.pitch
32✔
183
;;
184

185
let acoustic_interval
186
      (t : t)
187
      ~from:{ Fingerboard_location.fingerboard_position = p1; string_number = s1 }
188
      ~to_:{ Fingerboard_location.fingerboard_position = p2; string_number = s2 }
189
  =
190
  let (_ : Vibrating_string.t) = vibrating_string_exn t s1 in
660,157✔
191
  let (_ : Vibrating_string.t) = vibrating_string_exn t s2 in
660,157✔
192
  let i1 = Roman_numeral.to_int s1
660,157✔
193
  and i2 = Roman_numeral.to_int s2 in
660,157✔
194
  let interval_between_strings = ref Acoustic_interval.unison in
195
  for i = min i1 i2 to max i1 i2 - 1 do
660,157✔
196
    interval_between_strings
644,518✔
197
    := Acoustic_interval.add
644,518✔
198
         !interval_between_strings
199
         t.intervals_going_down.(i - 1).acoustic_interval
200
  done;
201
  Acoustic_interval.remove
202
    (Acoustic_interval.add
660,157✔
203
       !interval_between_strings
204
       (Fingerboard_position.acoustic_interval_to_the_open_string p2))
660,157✔
205
    (Fingerboard_position.acoustic_interval_to_the_open_string p1)
660,157✔
206
;;
207

UNCOV
208
let fingerboard_positions t = t.fingerboard_positions
×
209

210
let find_fingerboard_position (t : t) ~name =
211
  List.find t.fingerboard_positions ~f:(fun fingerboard_position ->
3,858✔
212
    String.equal name (Fingerboard_position.name fingerboard_position))
106,060✔
213
;;
214

215
let find_fingerboard_position_exn t ~name =
216
  match find_fingerboard_position t ~name with
1,282✔
217
  | Some x -> x
1,282✔
NEW
218
  | None ->
×
NEW
219
    Code_error.raise "Fingerboard_position not found." [ "name", name |> Dyn.string ]
×
220
;;
221

222
let add_fingerboard_position_exn
223
      ?(on_n_octaves = 3)
2,576✔
224
      (t : t)
225
      (fingerboard_position : Fingerboard_position.t)
226
  =
227
  if
2,576✔
228
    Acoustic_interval.compare
2,576✔
229
      (Fingerboard_position.acoustic_interval_to_the_open_string fingerboard_position)
2,576✔
230
      Acoustic_interval.octave
231
    > 0
232
  then
NEW
233
    Code_error.raise
×
234
      "Interval out of bounds."
NEW
235
      [ "fingerboard_position", fingerboard_position |> Fingerboard_position.to_dyn ];
×
236
  let name = Fingerboard_position.name fingerboard_position in
2,576✔
237
  (match find_fingerboard_position t ~name with
2,576✔
238
   | None -> ()
2,576✔
239
   | Some existing_fingerboard_position ->
×
NEW
240
     Code_error.raise
×
241
       "Duplicated fingerboard position's name."
NEW
242
       [ "name", name |> Dyn.string
×
NEW
243
       ; "fingerboard_position", fingerboard_position |> Fingerboard_position.to_dyn
×
244
       ; ( "existing_fingerboard_position"
NEW
245
         , existing_fingerboard_position |> Fingerboard_position.to_dyn )
×
246
       ]);
247
  let fingerboard_positions =
248
    List.init on_n_octaves ~f:(fun i ->
2,576✔
249
      Fingerboard_position.at_octave fingerboard_position ~octave:i)
7,728✔
250
    @ t.fingerboard_positions
251
    |> List.sort
252
         ~compare:
253
           (Comparable.lift
2,576✔
254
              Acoustic_interval.compare
255
              ~f:Fingerboard_position.acoustic_interval_to_the_open_string)
256
  in
257
  t.fingerboard_positions <- fingerboard_positions
2,576✔
258
;;
259

260
let exists_fingerboard_position t fingerboard_position =
UNCOV
261
  List.exists t.fingerboard_positions ~f:(fun p ->
×
UNCOV
262
    Fingerboard_position.equal p fingerboard_position)
×
263
;;
264

265
let exists_fingerboard_location
266
      t
267
      { Fingerboard_location.fingerboard_position; string_number }
268
  =
269
  let index = Roman_numeral.to_int string_number - 1 in
×
270
  index >= 0
UNCOV
271
  && index < Array.length t.vibrating_strings
×
UNCOV
272
  && exists_fingerboard_position t fingerboard_position
×
273
;;
274

275
let find_next_located_note
276
      (t : t)
277
      { Located_note.note; fingerboard_location }
278
      (characterized_interval : Characterized_interval.t)
279
  =
280
  let open Option.Let_syntax in
7,981✔
281
  let index = Roman_numeral.to_int fingerboard_location.string_number in
282
  let%bind fingerboard_location =
283
    List.find_map (List.init index ~f:Fn.id) ~f:(fun index ->
7,981✔
284
      let string_number = Roman_numeral.of_int_exn (index + 1) in
12,011✔
285
      match
12,011✔
286
        List.find t.fingerboard_positions ~f:(fun fingerboard_position ->
287
          match
495,955✔
288
            acoustic_interval
289
              t
290
              ~from:fingerboard_location
291
              ~to_:{ fingerboard_position; string_number }
292
          with
293
          | None -> false
166,624✔
294
          | Some found_interval ->
329,331✔
295
            Acoustic_interval.equal
296
              found_interval
297
              characterized_interval.acoustic_interval)
298
      with
299
      | None -> None
4,046✔
300
      | Some fingerboard_position ->
7,965✔
301
        Some { Fingerboard_location.fingerboard_position; string_number })
302
  in
303
  let%bind note = Interval.shift_up characterized_interval.interval note in
7,965✔
304
  return { Located_note.note; fingerboard_location }
7,965✔
305
;;
306

307
let open_string t string_number =
308
  let open Option.Let_syntax in
192✔
309
  let index = Roman_numeral.to_int string_number - 1 in
192✔
310
  let%bind vibrating_string =
311
    if index >= 0 && index < Array.length t.vibrating_strings
192✔
312
    then return t.vibrating_strings.(index)
192✔
UNCOV
313
    else None
×
314
  in
315
  let%bind fingerboard_position =
316
    match List.hd t.fingerboard_positions with
UNCOV
317
    | None -> None
×
318
    | Some fingerboard_position ->
192✔
319
      if
320
        Acoustic_interval.equal
321
          Acoustic_interval.unison
322
          (Fingerboard_position.acoustic_interval_to_the_open_string fingerboard_position)
192✔
323
      then return fingerboard_position
192✔
UNCOV
324
      else None
×
325
  in
326
  return
192✔
327
    { Located_note.note = vibrating_string.open_string
328
    ; fingerboard_location = { fingerboard_position; string_number }
329
    }
330
;;
331

332
let make_scale t ~characterized_scale ~from ~to_ =
333
  let rec aux acc scale (located_note : Located_note.t) =
304✔
334
    if Option.is_some (Interval.compute ~from:to_ ~to_:located_note.note ())
9,551✔
335
    then acc
288✔
336
    else (
9,263✔
337
      match scale with
338
      | [] -> aux acc characterized_scale located_note
1,282✔
339
      | hd :: tl ->
7,981✔
340
        (match find_next_located_note t located_note hd with
341
         | None -> acc
16✔
342
         | Some next_located_note -> aux (next_located_note :: acc) tl next_located_note))
7,965✔
343
  in
344
  aux [ from ] [] from |> List.rev
304✔
345
;;
346

347
let find_same_note_one_string_down t { Located_note.note; fingerboard_location } =
348
  let exception No_string_down in
3,557✔
349
  match
350
    let string_number =
351
      let index = Roman_numeral.to_int fingerboard_location.string_number in
352
      if index >= Array.length t.vibrating_strings
3,557✔
UNCOV
353
      then Stdlib.raise_notrace No_string_down
×
354
      else Roman_numeral.of_int_exn (index + 1)
3,508✔
355
    in
356
    match
357
      List.find t.fingerboard_positions ~f:(fun fingerboard_position ->
358
        match
133,578✔
359
          acoustic_interval
360
            t
361
            ~from:{ fingerboard_position; string_number }
362
            ~to_:fingerboard_location
363
        with
364
        | None -> false
1,094✔
365
        | Some found_interval ->
132,484✔
366
          Acoustic_interval.equal found_interval Acoustic_interval.unison)
367
    with
368
    | None -> None
40✔
369
    | Some fingerboard_position ->
3,468✔
370
      Some
371
        { Located_note.note
372
        ; fingerboard_location = { fingerboard_position; string_number }
373
        }
374
  with
375
  | res -> res
3,508✔
376
  | exception No_string_down -> None
49✔
377
;;
378

379
module Double_stops = struct
380
  type system = t
381
  type t = Double_stop.t list
382

383
  let to_ascii_table (system : system) double_stops =
384
    let columns =
202✔
385
      let open Print_table.O in
386
      let common_columns ~name ~(f : Double_stop.t -> Located_note.t) =
387
        [ Column.make ~header:name (fun t -> Cell.text (Note.to_string (f t).note))
404✔
388
        ; Column.make ~header:"String" (fun t ->
404✔
389
            Cell.text (Roman_numeral.to_string (f t).fingerboard_location.string_number))
9,462✔
390
        ; Column.make ~header:"Pos" (fun t ->
404✔
391
            Cell.text
9,462✔
392
              (Fingerboard_position.to_string
9,462✔
393
                 (f t).fingerboard_location.fingerboard_position))
9,462✔
394
        ; Column.make ~align:Right ~header:"Cents" (fun t ->
404✔
395
            let acoustic_interval =
9,462✔
396
              Fingerboard_position.acoustic_interval_to_the_open_string
397
                (f t).fingerboard_location.fingerboard_position
9,462✔
398
            in
399
            let cents = Acoustic_interval.to_cents acoustic_interval in
9,462✔
400
            Cell.text (Cents.to_string_nearest cents))
9,462✔
401
        ]
402
      in
403
      [ common_columns ~name:"Low" ~f:(fun (t : Double_stop.t) -> t.low_note)
18,924✔
404
      ; common_columns ~name:"High" ~f:(fun (t : Double_stop.t) -> t.high_note)
18,924✔
405
      ; [ Column.make ~header:"Interval" (fun (t : Double_stop.t) ->
202✔
406
            let interval =
4,731✔
407
              Interval.compute ~from:t.low_note.note ~to_:t.high_note.note ()
408
              |> Option.value_exn ~here:[%here]
4,731✔
409
            in
410
            let acoustic_interval =
4,731✔
411
              acoustic_interval
412
                system
413
                ~from:t.low_note.fingerboard_location
414
                ~to_:t.high_note.fingerboard_location
415
              |> Option.value_exn ~here:[%here]
4,731✔
416
            in
417
            Cell.text
4,731✔
418
              (Printf.sprintf
4,731✔
419
                 "%s - %s"
420
                 (Interval.to_string interval)
4,731✔
421
                 (Acoustic_interval.to_string acoustic_interval)))
4,731✔
422
        ; Column.make ~align:Right ~header:"Cents" (fun (t : Double_stop.t) ->
202✔
423
            let acoustic_interval =
4,731✔
424
              acoustic_interval
425
                system
426
                ~from:t.low_note.fingerboard_location
427
                ~to_:t.high_note.fingerboard_location
428
              |> Option.value_exn ~here:[%here]
4,731✔
429
            in
430
            Cell.text
4,731✔
431
              (Cents.to_string_nearest (Acoustic_interval.to_cents acoustic_interval)))
4,731✔
432
        ]
433
      ]
434
      |> List.concat
202✔
435
    in
436
    Print_table.to_string_text (Print_table.make ~columns ~rows:double_stops)
437
  ;;
438

439
  module Adjustment = struct
440
    type t =
441
      { from : Acoustic_interval.t
442
      ; to_ : Acoustic_interval.t
443
      }
444

445
    module Choice_criteria = struct
446
      (* The type is minted in such a way that the compare function
447
         must prioritize the lower values. *)
448
      type t =
449
        { exists_open_string_with_that_note : bool
114✔
450
        ; degree_priority : int
451
        }
452
      [@@deriving compare]
453

454
      let of_located_note (system : system) ~tonic (located_note : Located_note.t) =
455
        { exists_open_string_with_that_note =
76✔
456
            Array.exists system.vibrating_strings ~f:(fun v ->
76✔
457
              Note.equal
230✔
458
                v.open_string
459
                { located_note.note with
460
                  octave_designation = v.open_string.octave_designation
461
                })
462
        ; degree_priority =
463
            (match Interval.compute ~from:tonic ~to_:located_note.note () with
464
             | None -> 10
×
465
             | Some interval ->
76✔
466
               (match interval.number with
467
                | Second | Third | Sixth -> 1
×
UNCOV
468
                | Seventh -> 2
×
UNCOV
469
                | Fourth | Fifth -> 3
×
UNCOV
470
                | Unison | Octave -> 4))
×
471
        }
472
      ;;
473
    end
474
  end
475

476
  let adjust (system : system) ~tonic ~adjustment:{ Adjustment.from; to_ } (t : t) =
477
    List.map t ~f:(fun ({ Double_stop.low_note; high_note } as double_stop) ->
56✔
478
      let actual_interval =
1,148✔
479
        acoustic_interval
480
          system
481
          ~from:low_note.fingerboard_location
482
          ~to_:high_note.fingerboard_location
483
        |> Option.value_exn ~here:[%here]
1,148✔
484
      in
485
      if not (Acoustic_interval.equal actual_interval from)
1,148✔
486
      then double_stop
982✔
487
      else (
166✔
488
        let adjusted_low_note =
489
          let string_number = low_note.fingerboard_location.string_number in
490
          match
491
            List.find system.fingerboard_positions ~f:(fun fingerboard_position ->
492
              match
10,069✔
493
                acoustic_interval
494
                  system
495
                  ~from:{ fingerboard_position; string_number }
496
                  ~to_:high_note.fingerboard_location
497
              with
498
              | None -> false
2,441✔
499
              | Some found_interval -> Acoustic_interval.equal found_interval to_)
7,628✔
500
          with
501
          | None -> None
61✔
502
          | Some fingerboard_position ->
105✔
503
            Some
504
              { low_note with
505
                fingerboard_location = { fingerboard_position; string_number }
506
              }
507
        in
508
        let adjusted_high_note =
509
          let string_number = high_note.fingerboard_location.string_number in
510
          match
511
            List.find system.fingerboard_positions ~f:(fun fingerboard_position ->
512
              match
9,939✔
513
                acoustic_interval
514
                  system
515
                  ~from:low_note.fingerboard_location
516
                  ~to_:{ fingerboard_position; string_number }
517
              with
518
              | None -> false
3,443✔
519
              | Some found_interval -> Acoustic_interval.equal found_interval to_)
6,496✔
520
          with
521
          | None -> None
67✔
522
          | Some fingerboard_position ->
99✔
523
            Some
524
              { high_note with
525
                fingerboard_location = { fingerboard_position; string_number }
526
              }
527
        in
528
        match adjusted_low_note, adjusted_high_note with
UNCOV
529
        | None, None -> (* No adjustment available. *) double_stop
×
530
        | Some low_note, None -> { Double_stop.low_note; high_note }
67✔
531
        | None, Some high_note -> { Double_stop.low_note; high_note }
61✔
532
        | Some adjusted_low_note, Some adjusted_high_note ->
38✔
533
          (match
534
             Adjustment.Choice_criteria.compare
535
               (Adjustment.Choice_criteria.of_located_note system ~tonic low_note)
38✔
536
               (Adjustment.Choice_criteria.of_located_note system ~tonic high_note)
38✔
537
             |> Ordering.of_int
38✔
538
           with
UNCOV
539
           | Less | Equal -> { Double_stop.low_note = adjusted_low_note; high_note }
×
540
           | Greater -> { Double_stop.low_note; high_note = adjusted_high_note })))
18✔
541
  ;;
542

543
  let make_scale ?adjustment (t : system) ~characterized_scale ~interval_number ~from ~to_
544
    =
545
    let scale = make_scale t ~characterized_scale ~from ~to_ in
202✔
546
    let double_stops =
202✔
547
      let index = Interval.Number.to_int interval_number - 1 in
202✔
548
      let rec aux acc = function
UNCOV
549
        | [] -> acc
×
550
        | low_note :: tl as scale ->
5,022✔
551
          (match List.nth scale index with
552
           | None -> acc
202✔
553
           | Some high_note ->
4,820✔
554
             let acc =
555
               let double_stop =
556
                 let low_index =
557
                   Roman_numeral.to_int
4,820✔
558
                     low_note.Located_note.fingerboard_location.string_number
559
                 and high_index =
560
                   Roman_numeral.to_int
4,820✔
561
                     high_note.Located_note.fingerboard_location.string_number
562
                 in
563
                 if low_index = high_index
564
                 then
565
                   find_same_note_one_string_down t low_note
566
                   |> Option.map ~f:(fun low_note -> { Double_stop.low_note; high_note })
3,315✔
567
                 else if low_index = high_index + 2
1,416✔
568
                 then
569
                   find_same_note_one_string_down t high_note
570
                   |> Option.map ~f:(fun high_note -> { Double_stop.low_note; high_note })
153✔
571
                 else Some { Double_stop.low_note; high_note }
1,263✔
572
               in
573
               match double_stop with
574
               | None -> acc
89✔
575
               | Some double_stop -> double_stop :: acc
4,731✔
576
             in
577
             aux acc tl)
578
      in
579
      aux [] scale |> List.rev
202✔
580
    in
581
    match adjustment with
582
    | None -> double_stops
146✔
583
    | Some adjustment -> adjust t ~tonic:from.note ~adjustment double_stops
56✔
584
  ;;
585
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc